From 8c6ef478d93e9f323d0089bf60e9ff242320d17b Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Sat, 2 Dec 2023 16:57:24 -0500 Subject: [PATCH] feat: goto-definitions for mapped access --- src/analyze.rs | 13 +- src/backend.rs | 8 +- src/index/record.rs | 4 + src/index/symbol.rs | 7 + src/main.rs | 29 ++-- src/model.rs | 304 ++++++++++++++++++++++++++++++++++++++- src/python.rs | 339 +++++++++----------------------------------- src/xml.rs | 2 +- 8 files changed, 408 insertions(+), 298 deletions(-) diff --git a/src/analyze.rs b/src/analyze.rs index b7709b8..b125ecd 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -6,7 +6,7 @@ use tree_sitter::{Node, QueryCursor}; use odoo_lsp::{ index::interner, - model::{FieldKind, ModelName}, + model::ModelName, utils::{ByteRange, Erase, PreTravel, RangeExt}, ImStr, }; @@ -323,13 +323,10 @@ impl Backend { }; let ident = String::from_utf8_lossy(ident); let ident = interner.get_or_intern(ident.as_ref()); - let entry = self.populate_field_names(model, &[])?; - let entry = block_on(entry); - let field = entry.fields.as_ref()?.get(&ident.into())?; - match field.kind { - FieldKind::Relational(model) => Some(Type::Model(interner.resolve(&model).into())), - FieldKind::Value => None, - } + block_on(self.index.models.populate_field_names(model, &[])?); + let relation = + block_on(self.index.models.normalize_field_relation(ident.into(), model.into()))?; + Some(Type::Model(interner.resolve(&relation).into())) } _ => None, } diff --git a/src/backend.rs b/src/backend.rs index 2fea47f..c09af59 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -232,7 +232,7 @@ impl Backend { } let model_key = interner().get_or_intern(&model); let range = char_range_to_lsp_range(range, rope).ok_or_else(|| diagnostic!("range"))?; - let Some(entry) = self.populate_field_names(model_key.into(), &[]) else { + let Some(entry) = self.index.models.populate_field_names(model_key.into(), &[]) else { return Ok(()); }; let entry = entry.await; @@ -373,7 +373,7 @@ impl Backend { pub async fn jump_def_field_name(&self, field: &str, model: &str) -> miette::Result> { let model_key = interner().get_or_intern(model); let field = some!(interner().get(field)); - let entry = some!(self.populate_field_names(model_key.into(), &[])).await; + let entry = some!(self.index.models.populate_field_names(model_key.into(), &[])).await; let field = some!(entry.fields.as_ref()).get(&field.into()); Ok(Some(some!(field).location.clone().into())) } @@ -391,7 +391,7 @@ impl Backend { ) -> miette::Result> { let model_key = interner().get_or_intern(model); let field = some!(interner().get(name)); - let fields = some!(self.populate_field_names(model_key.into(), &[])).await; + let fields = some!(self.index.models.populate_field_names(model_key.into(), &[])).await; let field = some!(fields.fields.as_ref()).get(&field.into()); Ok(Some(Hover { range, @@ -526,7 +526,7 @@ impl Backend { let type_ = interner().resolve(&field.type_); if signature { match &field.kind { - FieldKind::Value => { + FieldKind::Value | FieldKind::Related(_) => { out = format!(concat!("```python\n", "{} = fields.{}(…)\n", "```\n\n"), name, type_) } FieldKind::Relational(relation) => { diff --git a/src/index/record.rs b/src/index/record.rs index a0900a5..4d11da1 100644 --- a/src/index/record.rs +++ b/src/index/record.rs @@ -132,6 +132,10 @@ impl SymbolMap { pub fn get(&self, key: &Symbol) -> Option<&T> { self.0.get(key.into_usize() as u64) } + #[inline] + pub fn get_mut(&mut self, key: &Symbol) -> Option<&mut T> { + self.0.get_mut(key.into_usize() as u64) + } pub fn keys(&self) -> impl Iterator> + '_ { self.0 .iter() diff --git a/src/index/symbol.rs b/src/index/symbol.rs index 9ce6b14..96cb321 100644 --- a/src/index/symbol.rs +++ b/src/index/symbol.rs @@ -35,6 +35,13 @@ impl From for Symbol { } } +impl From> for Spur { + #[inline] + fn from(value: Symbol) -> Self { + value.inner + } +} + impl Clone for Symbol { #[inline] fn clone(&self) -> Self { diff --git a/src/main.rs b/src/main.rs index 2350e6e..6b17317 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,6 @@ use tower_lsp::{LanguageServer, LspService, Server}; use odoo_lsp::config::Config; use odoo_lsp::index::{interner, Interner}; -use odoo_lsp::model::FieldKind; use odoo_lsp::{format_loc, some, utils::*}; mod analyze; @@ -462,7 +461,7 @@ impl LanguageServer for Backend { })) } Some(CompletionItemKind::FIELD) => { - // NOTE: This was injected by complete(). + // NOTE: This was injected by complete_field_name(). let Some(Value::String(value)) = &completion.data else { break 'resolve; }; @@ -473,22 +472,32 @@ impl LanguageServer for Backend { let Some(model) = interner().get(value) else { break 'resolve; }; - let Some(entry) = self.index.models.get(&model.into()) else { + let Some(mut entry) = self + .index + .models + .try_get_mut(&model.into()) + .expect(format_loc!("deadlock")) + else { + break 'resolve; + }; + let Some(fields) = &mut entry.fields else { break 'resolve; }; - let Some(fields) = &entry.fields else { break 'resolve }; - if let Some(field) = fields.get(&field.into()) { - let type_ = interner().resolve(&field.type_); - completion.detail = match field.kind { - FieldKind::Value => Some(format!("{type_}(…)")), - FieldKind::Relational(relation) => { + let field_entry = fields.get(&field.into()).cloned(); + drop(entry); + if let Some(field_entry) = field_entry { + let type_ = interner().resolve(&field_entry.type_); + completion.detail = match self.index.models.normalize_field_relation(field.into(), model).await + { + None => Some(format!("{type_}(…)")), + Some(relation) => { let relation = interner().resolve(&relation); Some(format!("{type_}(\"{relation}\", …)")) } }; completion.documentation = Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, - value: self.field_docstring(field_name, field, false), + value: self.field_docstring(field_name, &field_entry, false), })) } } diff --git a/src/model.rs b/src/model.rs index 38ce474..ca1907f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,19 +1,23 @@ +use std::borrow::Cow; use std::fmt::Display; +use std::future::ready; use std::ops::Deref; +use dashmap::mapref::one::RefMut; use dashmap::DashMap; -use lasso::Spur; -use log::debug; +use lasso::{Key, Spur}; +use log::{debug, warn}; use miette::{diagnostic, IntoDiagnostic}; use qp_trie::{wrapper::BString, Trie}; use tokio::sync::RwLock; use tower_lsp::lsp_types::Range; -use tree_sitter::{Parser, QueryCursor}; +use tree_sitter::{Node, Parser, QueryCursor}; use ts_macros::query; use crate::index::{interner, Interner, Symbol, SymbolMap}; use crate::str::Text; -use crate::utils::{ByteOffset, ByteRange, Erase, MinLoc, TryResultExt}; +use crate::utils::future::FutureOr; +use crate::utils::{ts_range_to_lsp_range, ByteOffset, ByteRange, Erase, MinLoc, RangeExt, TryResultExt}; use crate::{format_loc, ImStr}; #[derive(Clone, Debug)] @@ -53,6 +57,7 @@ pub enum FieldName {} pub enum FieldKind { Value, Relational(Spur), + Related(ImStr), } #[derive(Clone, Debug)] @@ -99,6 +104,23 @@ impl Deref for ModelIndex { } } +query! { + ModelFields(FIELD, TYPE, RELATION, ARG, VALUE); +r#" +((class_definition + (block + (expression_statement + (assignment + (identifier) @FIELD + (call [ + (identifier) @TYPE + (attribute (identifier) @_fields (identifier) @TYPE) + ] (argument_list . (string)? @RELATION + ((keyword_argument (identifier) @ARG (_) @VALUE) ","?)*)))))) +(#eq? @_fields "fields") +(#match? @TYPE "^[A-Z]"))"# +} + impl ModelIndex { pub async fn append(&self, path: Spur, interner: &Interner, replace: bool, items: &[Model]) { let mut by_prefix = self.by_prefix.write().await; @@ -161,6 +183,248 @@ impl ModelIndex { } } } + pub fn populate_field_names<'model>( + &'model self, + model: ModelName, + locations_filter: &'model [Spur], + ) -> Option>> { + let model_name = interner().resolve(&model); + let entry = self.try_get_mut(&model).expect(format_loc!("deadlock"))?; + if entry.fields.is_some() && locations_filter.is_empty() { + return Some(FutureOr::Ready(ready(entry))); + } + let t0 = std::time::Instant::now(); + let locations = entry.base.iter().chain(&entry.descendants).cloned().collect::>(); + + let query = ModelFields::query(); + let mut tasks = tokio::task::JoinSet::>::new(); + for ModelLocation(location, byte_range) in locations { + if !locations_filter.is_empty() && !locations_filter.contains(&location.path) { + continue; + } + tasks.spawn(async move { + let mut fields = vec![]; + let contents = tokio::fs::read(interner().resolve(&location.path)) + .await + .into_diagnostic()?; + let mut parser = Parser::new(); + parser.set_language(tree_sitter_python::language()).into_diagnostic()?; + let ast = parser + .parse(&contents, None) + .ok_or_else(|| diagnostic!("AST not built"))?; + let byte_range = byte_range.erase(); + let mut cursor = QueryCursor::new(); + cursor.set_byte_range(byte_range); + for match_ in cursor.matches(query, ast.root_node(), &contents[..]) { + let mut field = None; + let mut type_ = None; + let mut is_relational = false; + let mut relation = None; + let mut kwarg = None::; + let mut help = None; + let mut related = None; + enum Kwargs { + ComodelName, + Help, + Related, + } + for capture in match_.captures { + if capture.index == ModelFields::FIELD { + field = Some(capture.node); + } else if capture.index == ModelFields::TYPE { + type_ = Some(capture.node.byte_range()); + // TODO: fields.Reference + is_relational = matches!( + &contents[capture.node.byte_range()], + b"One2many" | b"Many2one" | b"Many2many" + ); + } else if capture.index == ModelFields::RELATION { + if is_relational { + relation = Some(capture.node.byte_range().shrink(1)); + } + } else if capture.index == ModelFields::ARG { + match &contents[capture.node.byte_range()] { + b"comodel_name" if is_relational => kwarg = Some(Kwargs::ComodelName), + b"help" => kwarg = Some(Kwargs::Help), + b"related" => kwarg = Some(Kwargs::Related), + _ => kwarg = None, + } + } else if capture.index == ModelFields::VALUE { + match kwarg { + Some(Kwargs::ComodelName) => { + if capture.node.kind() == "string" { + relation = Some(capture.node.byte_range().shrink(1)); + } + } + Some(Kwargs::Help) => { + if matches!(capture.node.kind(), "string" | "concatenated_string") { + help = Some(parse_help(&capture.node, &contents)); + } + } + Some(Kwargs::Related) => { + if capture.node.kind() == "string" { + related = Some(capture.node.byte_range().shrink(1)); + } + } + None => {} + } + } + } + if let (Some(field), Some(type_)) = (field, type_) { + let range = ts_range_to_lsp_range(field.range()); + let field_str = String::from_utf8_lossy(&contents[field.byte_range()]); + let field = interner().get_or_intern(&field_str); + let type_ = String::from_utf8_lossy(&contents[type_]); + let location = MinLoc { + path: location.path, + range, + }; + let help = help.map(|help| Text::try_from(help.as_ref()).unwrap()); + let kind = if let Some(relation) = relation { + let relation = String::from_utf8_lossy(&contents[relation]); + let relation = interner().get_or_intern(&relation); + FieldKind::Relational(relation) + } else if let Some(related) = related { + FieldKind::Related(String::from_utf8_lossy(&contents[related]).as_ref().into()) + } else { + if is_relational { + debug!("is_relational but no relation found: field={field_str} type={type_}"); + } + FieldKind::Value + }; + let type_ = interner().get_or_intern(&type_); + fields.push(( + field, + Field { + kind, + type_, + location, + help, + }, + )) + } + } + Ok(fields) + }); + } + Some(FutureOr::Pending(Box::pin(async move { + let ancestors = entry.ancestors.iter().cloned().collect::>(); + let mut out = SymbolMap::default(); + let mut fields_set = qp_trie::Trie::new(); + + // drop to prevent deadlock + drop(entry); + + // recursively get or populate ancestors' fields + for ancestor in ancestors { + if let Some(entry) = self.populate_field_names(ancestor, locations_filter) { + let entry = entry.await; + if let Some(fields) = entry.fields.as_ref() { + // TODO: Implement copy-on-write to increase reuse + out.extend(fields.iter().map(|(key, val)| (key.clone(), val.clone()))); + } + } + } + + while let Some(fields) = tasks.join_next().await { + let fields = match fields { + Ok(Ok(ret)) => ret, + Ok(Err(err)) => { + warn!("{err}"); + continue; + } + Err(err) => { + warn!("join error {err}"); + continue; + } + }; + for (key, type_) in fields { + out.insert_checked(key.into_usize() as u64, type_); + fields_set.insert_str(interner().resolve(&key), ()); + } + } + + log::info!( + "[{}] Populated {} fields in {}ms", + model_name, + out.len(), + t0.elapsed().as_millis() + ); + let mut entry = self + .try_get_mut(&model) + .expect(format_loc!("deadlock")) + .expect(format_loc!("no entry")); + entry.fields = Some(out); + entry.fields_set = fields_set; + entry + }))) + } + /// For completing a `Model.write({'foo.bar.baz': ..})`: + /// - `model` is the key of `Model` + /// - `needle` is a left substring of 'foo.bar.baz' + /// - `range` spans the entire range of 'foo.bar.baz' + /// + /// Returns None if resolution fails before `needle` is exhausted. + pub async fn resolve_mapped( + &self, + model: &mut Spur, + needle: &mut &str, + mut range: Option<&mut ByteRange>, + ) -> Option<()> { + while let Some((lhs, rhs)) = needle.split_once('.') { + debug!("mapped {needle}"); + // lhs: foo + // rhs: ba + *needle = rhs; + let fields = self.populate_field_names(model.clone().into(), &[])?.await; + let field = interner().get(&lhs)?; + let field = fields.fields.as_ref()?.get(&field.into()); + let FieldKind::Relational(rel) = field?.kind else { + return None; + }; + *model = rel; + // old range: foo.bar.baz + // range: bar.baz + if let Some(range) = range.as_mut() { + let start = range.start.0 + lhs.len() + 1; + **range = ByteOffset(start)..range.end; + } + } + Some(()) + } + pub async fn normalize_field_relation(&self, field: Symbol, model: Spur) -> Option { + let model_entry = self.get(&model.into())?; + let field_entry = model_entry.fields.as_ref()?.get(&field)?; + let mut kind = field_entry.kind.clone(); + let mut field_model = model.clone(); + if let FieldKind::Related(related) = &field_entry.kind { + debug!( + "related={related} field={} model={}", + interner().resolve(&field), + interner().resolve(&model) + ); + let related = related.clone(); + let mut related = related.as_str(); + drop(model_entry); + if self + .resolve_mapped(&mut field_model, &mut related, None) + .await + .is_some() + { + kind = FieldKind::Relational(field_model.into()); + let mut model_entry = self.try_get_mut(&model.into()).expect(format_loc!("deadlock"))?; + model_entry.fields.as_mut()?.get_mut(&field)?.kind = kind.clone(); + } else { + warn!("(normalize_field_kind) failed to normalize {related}"); + } + } + + match kind { + FieldKind::Relational(rel) => Some(rel.clone()), + FieldKind::Value => None, + FieldKind::Related(_) => None, + } + } } query! { @@ -200,3 +464,35 @@ impl ModelEntry { Ok(()) } } + +/// `node` must be `[(string) (concatenated_string)]` +fn parse_help<'text>(node: &Node, contents: &'text [u8]) -> Cow<'text, str> { + let mut cursor = node.walk(); + match node.kind() { + "string" => { + let content = node + .children(&mut cursor) + .find_map(|child| (child.kind() == "string_content").then(|| &contents[child.byte_range()])); + String::from_utf8_lossy(content.unwrap_or(&[])) + } + "concatenated_string" => { + let mut content = vec![]; + for string in node.children(&mut cursor) { + if string.kind() == "string" { + let mut cursor = string.walk(); + let children = string.children(&mut cursor).find_map(|child| { + (child.kind() == "string_content").then(|| { + String::from_utf8_lossy(&contents[child.byte_range()]) + .trim() + .replace("\\n", " \n") + .replace("\\t", "\t") + }) + }); + content.extend(children); + } + } + Cow::from(content.join(" ")) + } + _ => unreachable!(), + } +} diff --git a/src/python.rs b/src/python.rs index ba6ff10..ed67ed3 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,20 +1,16 @@ use crate::{Backend, Text}; use std::borrow::Cow; -use std::future::ready; use std::path::Path; -use dashmap::mapref::one::RefMut; -use lasso::{Key, Spur}; -use log::{debug, error, warn}; -use miette::{diagnostic, IntoDiagnostic}; -use odoo_lsp::index::{index_models, interner, Module, Symbol, SymbolMap}; -use odoo_lsp::utils::future::FutureOr; +use log::{debug, error}; +use miette::diagnostic; +use odoo_lsp::index::{index_models, interner, Module, Symbol}; use ropey::Rope; use tower_lsp::lsp_types::*; -use tree_sitter::{Node, Parser, QueryCursor, Tree}; +use tree_sitter::{Node, Parser, Tree}; -use odoo_lsp::model::{Field, FieldKind, ModelEntry, ModelLocation, ModelName, ModelType}; +use odoo_lsp::model::{ModelName, ModelType}; use odoo_lsp::utils::*; use odoo_lsp::{format_loc, some}; use ts_macros::query; @@ -113,55 +109,6 @@ r#" (attribute (_) (identifier) @ACCESS)"# } -query! { - ModelFields(FIELD, TYPE, RELATION, ARG, VALUE); -r#" -((class_definition - (block - (expression_statement - (assignment - (identifier) @FIELD - (call [ - (identifier) @TYPE - (attribute (identifier) @_fields (identifier) @TYPE) - ] (argument_list . (string)? @RELATION - ((keyword_argument (identifier) @ARG (_) @VALUE) ","?)*)))))) -(#eq? @_fields "fields") -(#match? @TYPE "^[A-Z]"))"# -} - -/// `node` must be `[(string) (concatenated_string)]` -fn parse_help<'text>(node: &Node, contents: &'text [u8]) -> Cow<'text, str> { - let mut cursor = node.walk(); - match node.kind() { - "string" => { - let content = node - .children(&mut cursor) - .find_map(|child| (child.kind() == "string_content").then(|| &contents[child.byte_range()])); - String::from_utf8_lossy(content.unwrap_or(&[])) - } - "concatenated_string" => { - let mut content = vec![]; - for string in node.children(&mut cursor) { - if string.kind() == "string" { - let mut cursor = string.walk(); - let children = string.children(&mut cursor).find_map(|child| { - (child.kind() == "string_content").then(|| { - String::from_utf8_lossy(&contents[child.byte_range()]) - .trim() - .replace("\\n", " \n") - .replace("\\t", "\t") - }) - }); - content.extend(children); - } - } - Cow::from(content.join(" ")) - } - _ => unreachable!(), - } -} - /// (module (_)*) fn top_level_stmt(module: Node, offset: usize) -> Option { module @@ -210,14 +157,14 @@ impl Backend { .map(|sym| interner().get_or_intern(&sym).into()) .collect(); drop(entry); - if let Some(fut) = self.populate_field_names(model_key.into(), &[path]) { + if let Some(fut) = self.index.models.populate_field_names(model_key.into(), &[path]) { fut.await; } } ModelType::Inherit(inherits) => { let Some(model) = inherits.first() else { continue }; let model_key = interner().get(model).unwrap(); - if let Some(fut) = self.populate_field_names(model_key.into(), &[path]) { + if let Some(fut) = self.index.models.populate_field_names(model_key.into(), &[path]) { fut.await; } } @@ -289,14 +236,13 @@ impl Backend { .nodes_for_capture_index(PyCompletions::PROP) .next() .map(|prop| { - dbg!(String::from_utf8_lossy(&contents[prop.byte_range()])); ( &contents[prop.byte_range()] == b"_inherit", &contents[prop.byte_range()] == b"_name", ) }) .unwrap_or((true, false)); - if is_inherit && !is_name && range.contains_end(offset) { + if is_inherit && range.contains_end(offset) { let Some(slice) = rope.get_byte_slice(range.clone()) else { dbg!(&range); break 'match_; @@ -389,18 +335,22 @@ impl Backend { items: items.into_inner(), }))); } - Some((EarlyReturn::Mapped { model, single_field }, needle, mut range, rope)) => { + Some((EarlyReturn::Mapped { model, single_field }, needle, range, rope)) => { // range: foo.bar.baz // needle: foo.ba let mut needle = needle.as_ref(); let mut model = some!(interner().get(&model)); + let mut range = range.map_unit(|unit| ByteOffset(rope.char_to_byte(unit.0))); if !single_field { some!( - self.resolve_mapped(&mut model, &mut needle, &mut range, rope.clone()) + self.index + .models + .resolve_mapped(&mut model, &mut needle, Some(&mut range)) .await ); } let model_name = interner().resolve(&model); + let range = range.map_unit(|unit| CharOffset(rope.byte_to_char(unit.0))); self.complete_field_name(needle, range, model_name.to_string(), rope, &mut items) .await?; return Ok(Some(CompletionResponse::List(CompletionList { @@ -466,38 +416,6 @@ impl Backend { range: range.map_unit(|unit| CharOffset(rope.byte_to_char(unit))), }) } - /// For completing a `Model.write({'foo.bar.baz': ..})`: - /// - `model` is the key of `Model` - /// - `needle` is a left substring of 'foo.bar.baz' - /// - `range` spans the entire range of 'foo.bar.baz' - /// - /// Returns None if resolution fails before `needle` is exhausted. - async fn resolve_mapped( - &self, - model: &mut Spur, - needle: &mut &str, - range: &mut CharRange, - rope: Rope, - ) -> Option<()> { - while let Some((lhs, rhs)) = needle.split_once('.') { - debug!("mapped {}", needle); - // lhs: foo - // rhs: ba - *needle = rhs; - let fields = self.populate_field_names(model.clone().into(), &[])?.await; - let field = interner().get(&lhs)?; - let field = fields.fields.as_ref()?.get(&field.into()); - let FieldKind::Relational(rel) = field?.kind else { - return None; - }; - *model = rel; - // old range: foo.bar.baz - // range: bar.baz - let start = rope.char_to_byte(range.start.0) + lhs.len() + 1; - *range = CharOffset(rope.byte_to_char(start))..range.end; - } - Some(()) - } pub async fn python_jump_def(&self, params: GotoDefinitionParams, rope: Rope) -> miette::Result> { let uri = ¶ms.text_document_position_params.text_document.uri; let ast = self @@ -509,11 +427,16 @@ impl Backend { Err(diagnostic!("could not find offset for {}", uri.path()))? }; let contents = rope.bytes().collect::>(); + enum EarlyReturn<'a> { + Access(Cow<'a, str>, &'a str), + Mapped(Mapped<'a>), + } let mut early_return = None; { let root = some!(top_level_stmt(ast.root_node(), offset)); let query = PyCompletions::query(); let mut cursor = tree_sitter::QueryCursor::new(); + let mut this_model = None; 'match_: for match_ in cursor.matches(query, root, &contents[..]) { for capture in match_.captures { if capture.index == PyCompletions::XML_ID { @@ -543,6 +466,13 @@ impl Backend { }; let slice = Cow::from(slice); return self.jump_def_model(&slice); + } else if range.end < offset && + // _inherit = '..' OR _inherit = ['..'] qualifies as primary model name + (is_meta && capture.node.parent() + .map(|parent| parent.kind() == "assignment" || (parent.kind() == "list" && parent.named_child_count() == 1)) + .unwrap_or(false)) + { + this_model = Some(&contents[capture.node.byte_range().shrink(1)]); } } else if capture.index == PyCompletions::ACCESS { let range = capture.node.byte_range(); @@ -552,15 +482,46 @@ impl Backend { let model = some!(self.model_of_range(root, lhs, None, &contents)); let field = String::from_utf8_lossy(&contents[range]); let model = interner().resolve(&model); - early_return = Some((field, model)); + early_return = Some(EarlyReturn::Access(field, model)); + break 'match_; + } + } else if capture.index == PyCompletions::MAPPED { + let range = capture.node.byte_range(); + if range.contains_end(offset) { + early_return = self + .gather_mapped(root, match_, offset, range.clone(), this_model, &rope, &contents, false) + .map(EarlyReturn::Mapped); break 'match_; } } } } } - if let Some((field, model)) = early_return { - return self.jump_def_field_name(&field, model).await; + match early_return { + Some(EarlyReturn::Access(field, model)) => { + return self.jump_def_field_name(&field, model).await; + } + Some(EarlyReturn::Mapped(Mapped { + needle, + model, + single_field, + range, + })) => { + let mut needle = needle.as_ref(); + let mut model = interner().get_or_intern(&model); + let mut range = range.map_unit(|unit| ByteOffset(rope.char_to_byte(unit.0))); + if !single_field { + some!( + self.index + .models + .resolve_mapped(&mut model, &mut needle, Some(&mut range)) + .await + ); + } + let model = interner().resolve(&model); + return self.jump_def_field_name(needle, model).await; + } + None => {} } Ok(None) } @@ -615,175 +576,6 @@ impl Backend { } Ok(None) } - pub fn populate_field_names<'model>( - &'model self, - model: ModelName, - locations_filter: &'model [Spur], - ) -> Option>> { - let model_name = interner().resolve(&model); - let entry = self.index.models.try_get_mut(&model).expect(format_loc!("deadlock"))?; - if entry.fields.is_some() && locations_filter.is_empty() { - return Some(FutureOr::Ready(ready(entry))); - } - let t0 = std::time::Instant::now(); - let locations = entry.base.iter().chain(&entry.descendants).cloned().collect::>(); - - let query = ModelFields::query(); - let mut tasks = tokio::task::JoinSet::>>::new(); - for ModelLocation(location, byte_range) in locations { - if !locations_filter.is_empty() && !locations_filter.contains(&location.path) { - continue; - } - tasks.spawn(async move { - let mut fields = vec![]; - let contents = tokio::fs::read(interner().resolve(&location.path)) - .await - .into_diagnostic()?; - let mut parser = Parser::new(); - parser.set_language(tree_sitter_python::language()).into_diagnostic()?; - let ast = parser - .parse(&contents, None) - .ok_or_else(|| diagnostic!("AST not built"))?; - let byte_range = byte_range.erase(); - let mut cursor = QueryCursor::new(); - cursor.set_byte_range(byte_range); - for match_ in cursor.matches(query, ast.root_node(), &contents[..]) { - let mut field = None; - let mut type_ = None; - let mut is_relational = false; - let mut relation = None; - let mut kwarg = None::; - let mut help = None; - enum Kwargs { - ComodelName, - Help, - } - for capture in match_.captures { - if capture.index == ModelFields::FIELD { - field = Some(capture.node); - } else if capture.index == ModelFields::TYPE { - type_ = Some(capture.node.byte_range()); - // TODO: fields.Reference - is_relational = matches!( - &contents[capture.node.byte_range()], - b"One2many" | b"Many2one" | b"Many2many" - ); - } else if capture.index == ModelFields::RELATION { - if is_relational { - relation = Some(capture.node.byte_range().shrink(1)); - } - } else if capture.index == ModelFields::ARG { - match &contents[capture.node.byte_range()] { - b"comodel_name" if is_relational => kwarg = Some(Kwargs::ComodelName), - b"help" => kwarg = Some(Kwargs::Help), - _ => kwarg = None, - } - } else if capture.index == ModelFields::VALUE { - match kwarg { - Some(Kwargs::ComodelName) => { - if capture.node.kind() == "string" { - relation = Some(capture.node.byte_range().shrink(1)); - } - } - Some(Kwargs::Help) => { - if matches!(capture.node.kind(), "string" | "concatenated_string") { - help = Some(parse_help(&capture.node, &contents)); - } - } - None => {} - } - } - } - if let (Some(field), Some(type_)) = (field, type_) { - let range = ts_range_to_lsp_range(field.range()); - let field_str = String::from_utf8_lossy(&contents[field.byte_range()]); - let field = interner().get_or_intern(&field_str); - let type_ = String::from_utf8_lossy(&contents[type_]); - let kind = if let Some(relation) = relation { - let relation = String::from_utf8_lossy(&contents[relation]); - let relation = interner().get_or_intern(&relation); - FieldKind::Relational(relation) - } else if is_relational { - debug!("is_relational but no relation found: field={field_str} type={type_}"); - continue; - } else { - FieldKind::Value - }; - let type_ = interner().get_or_intern(&type_); - let location = MinLoc { - path: location.path, - range, - }; - let help = help.map(|help| odoo_lsp::str::Text::try_from(help.as_ref()).unwrap()); - fields.push(( - field, - Field { - kind, - type_, - location, - help, - }, - )) - } - } - Ok(fields) - }); - } - Some(FutureOr::Pending(Box::pin(async move { - let ancestors = entry.ancestors.iter().cloned().collect::>(); - let mut out = SymbolMap::default(); - let mut fields_set = qp_trie::Trie::new(); - - // drop to prevent deadlock - drop(entry); - - // recursively get or populate ancestors' fields - for ancestor in ancestors { - if let Some(entry) = self.populate_field_names(ancestor, locations_filter) { - let entry = entry.await; - if let Some(fields) = entry.fields.as_ref() { - // TODO: Implement copy-on-write to increase reuse - out.extend(fields.iter().map(|(key, val)| (key.clone(), val.clone()))); - } - } - } - - while let Some(fields) = tasks.join_next().await { - let fields = match fields { - Ok(Ok(ret)) => ret, - Ok(Err(err)) => { - warn!("{err}"); - continue; - } - Err(err) => { - warn!("join error {err}"); - continue; - } - }; - for (key, type_) in fields { - out.insert_checked(key.into_usize() as u64, type_); - fields_set.insert_str(interner().resolve(&key), ()); - } - } - - log::info!( - "[{}] Populated {} fields in {}ms", - model_name, - out.len(), - t0.elapsed().as_millis() - ); - - let mut entry = self - .index - .models - .try_get_mut(&model) - .expect(format_loc!("deadlock")) - .expect(format_loc!("no entry")); - entry.fields = Some(out); - entry.fields_set = fields_set; - entry - }))) - } pub async fn python_hover(&self, params: HoverParams, rope: Rope) -> miette::Result> { let uri = ¶ms.text_document_position_params.text_document.uri; @@ -892,19 +684,22 @@ impl Backend { needle, model, single_field, - mut range, + range, })) => { let mut needle = needle.as_ref(); let mut model = interner().get_or_intern(&model); + let mut range = range.map_unit(|unit| ByteOffset(rope.char_to_byte(unit.0))); if !single_field { some!( - self.resolve_mapped(&mut model, &mut needle, &mut range, rope.clone()) + self.index + .models + .resolve_mapped(&mut model, &mut needle, Some(&mut range)) .await ); } let model = interner().resolve(&model); return self - .hover_field_name(needle, model, char_range_to_lsp_range(range, rope.clone())) + .hover_field_name(needle, model, offset_range_to_lsp_range(range, rope.clone())) .await; } None => {} @@ -923,7 +718,9 @@ impl Backend { #[cfg(test)] mod tests { use super::*; + use odoo_lsp::model::ModelFields; use pretty_assertions::assert_eq; + use tree_sitter::QueryCursor; /// Tricky behavior here. The query syntax must match the trailing comma between /// named arguments, and this test checks that. Furthermore, @help cannot be matched diff --git a/src/xml.rs b/src/xml.rs index f113a6d..f56086a 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -190,7 +190,7 @@ impl Backend { RefKind::Ref(relation) => { let model_key = some!(model_filter); let model = some!(interner().get(&model_key)); - let fields = some!(self.populate_field_names(model.into(), &[])).await; + let fields = some!(self.index.models.populate_field_names(model.into(), &[])).await; let fields = some!(fields.fields.as_ref()); let Some(Field { kind: FieldKind::Relational(relation),