Skip to content

Commit d5cf8f6

Browse files
committed
feat: go-to Owl components and templates
1 parent 2527bfb commit d5cf8f6

File tree

6 files changed

+117
-38
lines changed

6 files changed

+117
-38
lines changed

src/backend.rs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use odoo_lsp::{format_loc, some, utils::*};
2929

3030
pub struct Backend {
3131
pub client: Client,
32+
/// fs path -> rope, diagnostics etc.
3233
pub document_map: DashMap<String, Document>,
3334
pub record_ranges: DashMap<String, Box<[ByteRange]>>,
3435
pub ast_map: DashMap<String, Tree>,
@@ -387,7 +388,7 @@ impl Backend {
387388
.map(|model| {
388389
let label = interner().resolve(model.key()).to_string();
389390
let module = model.base.as_ref().and_then(|base| {
390-
let module = self.index.module_of_path(&base.0.path.as_path())?;
391+
let module = self.index.module_of_path(&base.0.path.to_path())?;
391392
Some(interner().resolve(&module).to_string())
392393
});
393394
CompletionItem {
@@ -419,20 +420,18 @@ impl Backend {
419420
offset_range_to_lsp_range(range, rope).ok_or_else(|| diagnostic!("(complete_template_name) range"))?;
420421
let interner = interner();
421422
let by_prefix = self.index.templates.by_prefix.read().await;
422-
let matches = by_prefix.iter_prefix(needle.as_bytes()).flat_map(|(_, templates)| {
423-
templates.iter().flat_map(|key| {
424-
let label = interner.resolve(key).to_string();
425-
Some(CompletionItem {
426-
text_edit: Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
427-
new_text: label.clone(),
428-
insert: range,
429-
replace: range,
430-
})),
431-
label,
432-
kind: Some(CompletionItemKind::REFERENCE),
433-
..Default::default()
434-
})
435-
})
423+
let matches = by_prefix.iter_prefix(needle.as_bytes()).map(|(_, key)| {
424+
let label = interner.resolve(key).to_string();
425+
CompletionItem {
426+
text_edit: Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
427+
new_text: label.clone(),
428+
insert: range,
429+
replace: range,
430+
})),
431+
label,
432+
kind: Some(CompletionItemKind::REFERENCE),
433+
..Default::default()
434+
}
436435
});
437436
items.extend(matches);
438437
Ok(())
@@ -520,7 +519,7 @@ impl Backend {
520519
let module = component
521520
.location
522521
.as_ref()
523-
.and_then(|loc| self.index.module_of_path(&loc.path.as_path()));
522+
.and_then(|loc| self.index.module_of_path(&loc.path.to_path()));
524523
let value = fomat!(
525524
"```js\n"
526525
"(component) class " (name) ";\n"
@@ -540,7 +539,7 @@ impl Backend {
540539
let module = template
541540
.location
542541
.as_ref()
543-
.and_then(|loc| self.index.module_of_path(&loc.path.as_path()));
542+
.and_then(|loc| self.index.module_of_path(&loc.path.to_path()));
544543
let value = fomat!(
545544
"```xml\n"
546545
"<t t-name=\"" (name) "\"/>\n"
@@ -683,13 +682,13 @@ impl Backend {
683682
let module = model
684683
.base
685684
.as_ref()
686-
.and_then(|base| self.index.module_of_path(&base.0.path.as_path()));
685+
.and_then(|base| self.index.module_of_path(&base.0.path.to_path()));
687686
let mut descendants = model
688687
.descendants
689688
.iter()
690689
.map(|loc| &loc.0)
691690
.scan(SymbolSet::default(), |mods, loc| {
692-
let Some(module) = self.index.module_of_path(&loc.path.as_path()) else {
691+
let Some(module) = self.index.module_of_path(&loc.path.to_path()) else {
693692
return Some(None);
694693
};
695694
if mods.insert(module) {
@@ -743,7 +742,7 @@ impl Backend {
743742
}
744743
if let Some(module) = self
745744
.index
746-
.module_of_path(&field.location.path.as_path())
745+
.module_of_path(&field.location.path.to_path())
747746
{
748747
"*Defined in:* `" (interner().resolve(&module)) "` \n"
749748
}

src/index/symbol.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ impl PathSymbol {
3232
let empty = interner().get_or_intern_static(".");
3333
PathSymbol(empty, empty)
3434
}
35-
pub fn as_path(&self) -> PathBuf {
35+
pub fn to_path(&self) -> PathBuf {
3636
let root = Path::new(interner().resolve(&self.0));
3737
root.join(interner().resolve(&self.1))
3838
}
39+
pub fn as_string(&self) -> String {
40+
let path = self.to_path();
41+
path.to_string_lossy().into_owned()
42+
}
3943
}
4044

4145
impl Display for PathSymbol {

src/index/template.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::collections::HashSet;
2-
31
use derive_more::Deref;
42

53
use dashmap::DashMap;
@@ -19,7 +17,7 @@ pub struct TemplateIndex {
1917
pub by_prefix: RwLock<TemplatePrefixTrie>,
2018
}
2119

22-
pub type TemplatePrefixTrie = qp_trie::Trie<&'static [u8], HashSet<TemplateName>>;
20+
pub type TemplatePrefixTrie = qp_trie::Trie<&'static [u8], TemplateName>;
2321

2422
impl TemplateIndex {
2523
pub async fn append(&self, entries: Vec<NewTemplate>) {
@@ -38,10 +36,7 @@ impl TemplateIndex {
3836
self.entry(entry.name).or_default().descendants.push(entry.template);
3937
}
4038
let raw = interner().resolve(&entry.name);
41-
prefix
42-
.entry(raw.as_bytes())
43-
.or_insert_with(Default::default)
44-
.insert(entry.name);
39+
prefix.insert(raw.as_bytes(), entry.name);
4540
}
4641
}
4742
pub(super) fn statistics(&self) -> serde_json::Value {

src/main.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ impl LanguageServer for Backend {
170170
references_provider: Some(OneOf::Left(true)),
171171
workspace_symbol_provider: Some(OneOf::Left(true)),
172172
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(Default::default())),
173+
// XML code actions are done in 1 pass only
174+
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
175+
execute_command_provider: Some(ExecuteCommandOptions {
176+
commands: vec!["goto_owl".to_string()],
177+
..Default::default()
178+
}),
173179
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
174180
change: Some(TextDocumentSyncKind::INCREMENTAL),
175181
save: Some(TextDocumentSyncSaveOptions::Supported(true)),
@@ -636,9 +642,6 @@ impl LanguageServer for Backend {
636642
}
637643
self.index.mark_n_sweep();
638644
}
639-
async fn execute_command(&self, _: ExecuteCommandParams) -> Result<Option<Value>> {
640-
Ok(None)
641-
}
642645
async fn symbol(&self, params: WorkspaceSymbolParams) -> Result<Option<Vec<SymbolInformation>>> {
643646
let query = &params.query;
644647

@@ -709,7 +712,7 @@ impl LanguageServer for Backend {
709712
damage_zone,
710713
&mut document.diagnostics_cache,
711714
);
712-
diagnostics = document.diagnostics_cache.clone();
715+
diagnostics.clone_from(&document.diagnostics_cache);
713716
}
714717
}
715718
Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
@@ -722,6 +725,43 @@ impl LanguageServer for Backend {
722725
},
723726
)))
724727
}
728+
async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
729+
let Some((_, "xml")) = params.text_document.uri.path().rsplit_once('.') else {
730+
return Ok(None);
731+
};
732+
733+
let document = some!(self.document_map.get(params.text_document.uri.path()));
734+
735+
Ok(self
736+
.xml_code_actions(params, document.rope.clone())
737+
.inspect_err(|err| {
738+
error!("(code_lens) {err}");
739+
})
740+
.unwrap_or(None))
741+
}
742+
async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<Value>> {
743+
match (params.command.as_str(), params.arguments.as_slice()) {
744+
("goto_owl", [Value::String(_), Value::String(subcomponent)]) => {
745+
// FIXME: Subcomponents should not just depend on the component's name,
746+
// since users can readjust subcomponents' names at will.
747+
let component = some!(interner().get(subcomponent));
748+
let component = some!(self.index.components.get(&component.into()));
749+
let location = some!(component.location.as_ref());
750+
_ = self
751+
.client
752+
.show_document(ShowDocumentParams {
753+
uri: Url::from_file_path(location.path.as_string()).unwrap(),
754+
external: Some(false),
755+
take_focus: Some(true),
756+
selection: Some(location.range),
757+
})
758+
.await;
759+
}
760+
_ => {}
761+
}
762+
763+
Ok(None)
764+
}
725765
}
726766

727767
#[tokio::main(flavor = "current_thread")]

src/model.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ impl ModelIndex {
224224
return None;
225225
}
226226
let mut fields = vec![];
227-
let fpath = location.path.as_path();
227+
let fpath = location.path.to_path();
228228
let contents = std::fs::read(&fpath)
229229
.map_err(|err| error!("Failed to read {}:\n{err}", fpath.display()))
230230
.ok()?;
@@ -513,7 +513,7 @@ impl ModelEntry {
513513
return Ok(());
514514
};
515515
if self.docstring.is_none() {
516-
let contents = tokio::fs::read(loc.path.as_path()).await.into_diagnostic()?;
516+
let contents = tokio::fs::read(loc.path.to_path()).await.into_diagnostic()?;
517517
let mut parser = Parser::new();
518518
parser.set_language(tree_sitter_python::language()).into_diagnostic()?;
519519
let ast = parser

src/xml.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::sync::Arc;
1010
use lasso::Spur;
1111
use log::{debug, warn};
1212
use miette::diagnostic;
13+
use odoo_lsp::component::ComponentTemplate;
1314
use odoo_lsp::index::{interner, PathSymbol};
1415
use odoo_lsp::model::{Field, FieldKind};
1516
use odoo_lsp::template::gather_templates;
@@ -37,6 +38,8 @@ enum RefKind<'a> {
3738
/// An arbitrary Python expression.
3839
/// Includes the relative offset of where the cursor is.
3940
PyExpr(usize),
41+
/// `<Component />`
42+
Component,
4043
}
4144

4245
enum Tag<'a> {
@@ -328,7 +331,7 @@ impl Backend {
328331
&mut items,
329332
)?;
330333
}
331-
RefKind::TName => return Ok(None),
334+
RefKind::TName | RefKind::Component => return Ok(None),
332335
}
333336

334337
Ok(Some(CompletionResponse::List(CompletionList {
@@ -384,6 +387,14 @@ impl Backend {
384387
let model = some!(self.resolve_type(&model, &Scope::default()));
385388
self.jump_def_field_name(&field, interner().resolve(&model))
386389
}
390+
Some(RefKind::Component) => {
391+
let component = some!(interner().get(needle));
392+
let component = some!(self.index.components.get(&component.into()));
393+
let Some(ComponentTemplate::Name(template)) = component.template.as_ref() else {
394+
return Ok(None);
395+
};
396+
self.jump_def_template_name(interner().resolve(template))
397+
}
387398
None => Ok(None),
388399
}
389400
}
@@ -413,7 +424,11 @@ impl Backend {
413424
Some(RefKind::Ref(_)) | Some(RefKind::Id) => self.record_references(cursor_value, current_module),
414425
Some(RefKind::TInherit) | Some(RefKind::TCall) => self.template_references(cursor_value, true),
415426
Some(RefKind::TName) => self.template_references(cursor_value, false),
416-
Some(RefKind::PyExpr(_)) | Some(RefKind::FieldName(_)) | Some(RefKind::PropOf(..)) | None => Ok(None),
427+
Some(RefKind::PyExpr(_))
428+
| Some(RefKind::FieldName(_))
429+
| Some(RefKind::PropOf(..))
430+
| Some(RefKind::Component)
431+
| None => Ok(None),
417432
}
418433
}
419434
pub fn xml_hover(&self, params: HoverParams, rope: Rope) -> miette::Result<Option<Hover>> {
@@ -513,7 +528,7 @@ impl Backend {
513528
offset_range_to_lsp_range(range.map_unit(|rel_unit| ByteOffset(rel_unit + anchor)), rope.clone()),
514529
)
515530
}
516-
Some(RefKind::TName) | Some(RefKind::PropOf(..)) | None => {
531+
Some(RefKind::TName) | Some(RefKind::PropOf(..)) | Some(RefKind::Component) | None => {
517532
#[cfg(not(debug_assertions))]
518533
return Ok(None);
519534

@@ -531,6 +546,28 @@ impl Backend {
531546
}
532547
}
533548
}
549+
pub fn xml_code_actions(&self, params: CodeActionParams, rope: Rope) -> miette::Result<Option<CodeActionResponse>> {
550+
let uri = &params.text_document.uri;
551+
let position = params.range.start;
552+
let (slice, offset_at_cursor, _) = self.record_slice(&rope, uri, position)?;
553+
let slice_str = Cow::from(slice);
554+
let mut reader = Tokenizer::from(slice_str.as_ref());
555+
556+
let XmlRefs {
557+
ref_at_cursor,
558+
ref_kind,
559+
..
560+
} = self.gather_refs(offset_at_cursor, &mut reader, &slice)?;
561+
let (Some((value, _)), Some(RefKind::Component)) = (ref_at_cursor, ref_kind) else {
562+
return Ok(None);
563+
};
564+
565+
Ok(Some(vec![CodeActionOrCommand::Command(Command {
566+
title: "Go to Owl component".to_string(),
567+
command: "goto_owl".to_string(),
568+
arguments: Some(vec![String::new().into(), value.into()]),
569+
})]))
570+
}
534571
/// The main function that determines all the information needed
535572
/// to resolve the symbol at the cursor.
536573
fn gather_refs<'read>(
@@ -569,7 +606,7 @@ impl Backend {
569606

570607
for token in reader {
571608
match token {
572-
Ok(Token::ElementStart { local, .. }) => {
609+
Ok(Token::ElementStart { local, prefix, .. }) => {
573610
expect_model_string = false;
574611
depth += 1;
575612
match local.as_str() {
@@ -585,6 +622,10 @@ impl Backend {
585622
}
586623
component if template_mode && component.starts_with(|c| char::is_ascii_uppercase(&c)) => {
587624
tag = Some(Tag::TComponent(component));
625+
if prefix.is_empty() && local.range().contains_end(offset_at_cursor) {
626+
ref_at_cursor = Some((local.as_str(), local.range()));
627+
ref_kind = Some(RefKind::Component);
628+
}
588629
}
589630
_ if arch_mode => tag = None,
590631
_ => {}

0 commit comments

Comments
 (0)