diff --git a/package.json b/package.json index 11aec69..47ee2a3 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "test-compile": "tsc -p ./", "compile": "cross-env NODE_ENV=production tsc -b", "watch": "rm -rf dist && tsc -b -w", - "lint": "prettier --write . && ruff format && cargo fmt && cargo clippy --fix --allow-dirty --allow-staged", + "lint": "prettier --write . && cargo fmt && cargo clippy --fix --allow-dirty --allow-staged && ruff format", "pretest": "npm run compile && npm run lint", "test": "node ./out/test/runTest.js", "build": "webpack --config webpack.config.js", diff --git a/src/analyze.rs b/src/analyze.rs index 5cd6a0c..327bf5e 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -3,7 +3,7 @@ use std::{borrow::Borrow, collections::HashMap, fmt::Debug, iter::FusedIterator, ops::ControlFlow}; -use tracing::trace; +use tracing::{instrument, trace}; use tree_sitter::{Node, QueryCursor}; use odoo_lsp::{ @@ -146,7 +146,7 @@ impl<'a> Iterator for Iter<'a> { #[rustfmt::skip] query! { #[derive(Debug)] - FieldCompletion(Name, SelfParam, Scope, Def); + FieldCompletion(Name, SelfParam, Scope); ((class_definition (block (expression_statement @@ -155,8 +155,8 @@ query! { [ (decorated_definition (function_definition - (parameters . (identifier) @SELF_PARAM) (block) @SCOPE) .) - (function_definition (parameters . (identifier) @SELF_PARAM) (block) @SCOPE)] @DEF)) @class + (parameters . (identifier) @SELF_PARAM)) @SCOPE) + (function_definition (parameters . (identifier) @SELF_PARAM)) @SCOPE])) @class (#match? @_name "^_(name|inherit)$")) } @@ -547,6 +547,7 @@ impl Backend { } /// Returns `(self_type, fn_scope, self_param)` +#[instrument(level = "trace", skip_all, ret)] pub fn determine_scope<'out, 'node>( node: Node<'node>, contents: &'out [u8], @@ -571,19 +572,17 @@ pub fn determine_scope<'out, 'node>( Some(FieldCompletion::SelfParam) => { self_param = Some(capture.node); } - Some(FieldCompletion::Def) => { + Some(FieldCompletion::Scope) => { if !capture.node.byte_range().contains_end(offset) { continue 'scoping; } - } - Some(FieldCompletion::Scope) => { fn_scope = Some(capture.node); } None => {} } } if fn_scope.is_some() { - break 'scoping; + break; } } let fn_scope = fn_scope?; @@ -617,7 +616,6 @@ class Foo(models.AbstractModel): let ast = parser.parse(&contents[..], None).unwrap(); let query = FieldCompletion::query(); let mut cursor = QueryCursor::new(); - // let expected: &[&[u32]] = &[]; let actual = cursor .matches(query, ast.root_node(), &contents[..]) .map(|match_| { @@ -635,22 +633,8 @@ class Foo(models.AbstractModel): matches!( &actual[..], [ - [ - None, - None, - Some(T::Name), - Some(T::Def), - Some(T::SelfParam), - Some(T::Scope) - ], - [ - None, - None, - Some(T::Name), - Some(T::Def), - Some(T::SelfParam), - Some(T::Scope) - ] + [None, None, Some(T::Name), Some(T::Scope), Some(T::SelfParam)], + [None, None, Some(T::Name), Some(T::Scope), Some(T::SelfParam)] ] ), "{actual:?}" diff --git a/src/python.rs b/src/python.rs index 920d221..ddfad5a 100644 --- a/src/python.rs +++ b/src/python.rs @@ -12,7 +12,7 @@ use miette::{diagnostic, miette}; use odoo_lsp::index::{index_models, interner, PathSymbol}; use ropey::Rope; use tower_lsp::lsp_types::*; -use tracing::{debug, trace, warn}; +use tracing::{debug, instrument, trace, warn}; use tree_sitter::{Node, Parser, QueryCursor, QueryMatch, Tree}; use odoo_lsp::model::{ModelName, ModelType, ResolveMappedError}; @@ -551,6 +551,7 @@ impl Backend { } /// Resolves the attribute at the cursor offset. /// Returns `(object, field, range)` + #[instrument(level = "trace", skip_all, ret)] pub fn attribute_node_at_offset<'out>( &'out self, mut offset: usize, @@ -562,8 +563,10 @@ impl Backend { } offset = offset.clamp(0, contents.len() - 1); let mut cursor_node = root.descendant_for_byte_range(offset, offset)?; + let mut real_offset = None; if cursor_node.is_named() && !matches!(cursor_node.kind(), "attribute" | "identifier") { // We got our cursor left in the middle of nowhere. + real_offset = Some(offset); offset = offset.saturating_sub(1); cursor_node = root.descendant_for_byte_range(offset, offset)?; } @@ -578,15 +581,12 @@ impl Backend { let rhs; if !cursor_node.is_named() { // We landed on one of the punctuations inside the attribute. - // Need to determine which one is it. + // Need to determine which one it is. let dot = cursor_node.descendant_for_byte_range(offset, offset)?; lhs = dot.prev_named_sibling()?; rhs = dot.next_named_sibling().and_then(|attr| match attr.kind() { "identifier" => Some(attr), - // TODO: Unwrap all layers of attributes - "attribute" => attr - .child_by_field_name("object") - .and_then(|obj| (obj.kind() == "identifier").then_some(obj)), + "attribute" => attr.child_by_field_name("attribute"), _ => None, }); } else if cursor_node.kind() == "attribute" { @@ -619,13 +619,15 @@ impl Backend { let Some(rhs) = rhs else { // In single-expression mode, rhs could be empty in which case // we return an empty needle/range. - return Some((lhs, Cow::from(""), offset + 1..offset + 1)); + let offset = real_offset.unwrap_or(offset); + return Some((lhs, Cow::from(""), offset..offset)); }; let (field, range) = if rhs.range().start_point.row != lhs.range().end_point.row { // tree-sitter has an issue with attributes spanning multiple lines // which is NOT valid Python, but allows it anyways because tree-sitter's // use cases don't require strict syntax trees. - (Cow::from(""), offset + 1..offset + 1) + let offset = real_offset.unwrap_or(offset); + (Cow::from(""), offset..offset) } else { let range = rhs.byte_range(); (String::from_utf8_lossy(&contents[range.clone()]), range) diff --git a/testing/conftest.py b/testing/conftest.py index b0feba1..73c8284 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -48,7 +48,9 @@ def setup(): ) async def client(lsp_client: LanguageClient, rootdir: str): params = InitializeParams( - workspace_folders=[WorkspaceFolder(uri=Path(rootdir).as_uri(), name="odoo-lsp")], + workspace_folders=[ + WorkspaceFolder(uri=Path(rootdir).as_uri(), name="odoo-lsp") + ], capabilities=ClientCapabilities( window=WindowClientCapabilities(work_done_progress=True), text_document=TextDocumentClientCapabilities( diff --git a/testing/fixtures/basic/bar/models.py b/testing/fixtures/basic/bar/models.py index 899d215..0748dfc 100644 --- a/testing/fixtures/basic/bar/models.py +++ b/testing/fixtures/basic/bar/models.py @@ -7,4 +7,4 @@ class DerivedBar(models.Model): _inherit = "bar" def test(self): - pass \ No newline at end of file + pass diff --git a/testing/fixtures/basic/foo/models.py b/testing/fixtures/basic/foo/models.py index d2b88cf..e46a1b8 100644 --- a/testing/fixtures/basic/foo/models.py +++ b/testing/fixtures/basic/foo/models.py @@ -6,6 +6,9 @@ class Foo(Model): def completions(self): self.env["bar"] # ^complete bar derived.bar foo foob + for foo in self: + foo. + # ^complete bar def diagnostics(self): self.foo @@ -17,7 +20,6 @@ def diagnostics(self): self.env["fo"] # ^diag `fo` is not a valid model name - class Foob(Model): _name = "foob" diff --git a/testing/fixtures/basic/test_main.py b/testing/fixtures/basic/test_main.py index 55222f6..097cb32 100644 --- a/testing/fixtures/basic/test_main.py +++ b/testing/fixtures/basic/test_main.py @@ -42,7 +42,9 @@ def __init__(self): @pytest.mark.asyncio(scope="module") async def test_python(client: LanguageClient, rootdir: str): - files = {file: file.read_text() for file in Path(rootdir).joinpath("foo").rglob("*.py")} + files = { + file: file.read_text() for file in Path(rootdir).joinpath("foo").rglob("*.py") + } expected = defaultdict[Path, Expected](Expected) asts = dict[Path, Tree]() for file, text in files.items(): @@ -54,11 +56,15 @@ async def test_python(client: LanguageClient, rootdir: str): text = text.decode() if (offset := text.find("^diag ")) != -1: msg = text[offset + 6 :].strip() - pos = Position(node.start_point.row - 1, node.start_point.column + offset) + pos = Position( + node.start_point.row - 1, node.start_point.column + offset + ) expected[file].diag.append((pos, msg)) elif (offset := text.find("^complete ")) != -1: completions = text[offset + 10 :].strip().split(" ") - pos = Position(node.start_point.row - 1, node.start_point.column + offset) + pos = Position( + node.start_point.row - 1, node.start_point.column + offset + ) expected[file].complete.append((pos, completions)) unexpected: list[str] = [] @@ -81,7 +87,9 @@ async def test_python(client: LanguageClient, rootdir: str): for missing in diff.pop("iterable_item_removed", {}).values(): # type: ignore unexpected.append(f"diag: missing {missing}\n at {file}") for mismatch in diff.pop("values_changed", {}).values(): # type: ignore - unexpected.append(f"diag: expected={mismatch['old_value']!r} actual={mismatch['new_value']!r}\n at {file}") + unexpected.append( + f"diag: expected={mismatch['old_value']!r} actual={mismatch['new_value']!r}\n at {file}" + ) if diff: unexpected.append(f"diag: unexpected {diff}\n at {file}") @@ -95,13 +103,17 @@ async def test_python(client: LanguageClient, rootdir: str): assert isinstance(results, CompletionList) actual = [e.label for e in results.items] if actual != expected_completion: - node = asts[file].root_node.named_descendant_for_point_range((pos.line, pos.character), (pos.line, 9999)) + node = asts[file].root_node.named_descendant_for_point_range( + (pos.line, pos.character), (pos.line, 9999) + ) assert node if text := node.text: node_text = text.decode() else: node_text = "" - unexpected.append(f"complete: actual={' '.join(actual)}\n at {file}:{pos}\n{' ' * node.start_point.column}{node_text}") + unexpected.append( + f"complete: actual={' '.join(actual)}\n at {file}:{pos}\n{' ' * node.start_point.column}{node_text}" + ) unexpected_len = len(unexpected) assert not unexpected_len, "\n".join(unexpected)