Skip to content

Commit 70af361

Browse files
committed
fix: support incomplete attributes
1 parent 8eaa51f commit 70af361

File tree

7 files changed

+45
-43
lines changed

7 files changed

+45
-43
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
"test-compile": "tsc -p ./",
160160
"compile": "cross-env NODE_ENV=production tsc -b",
161161
"watch": "rm -rf dist && tsc -b -w",
162-
"lint": "prettier --write . && ruff format && cargo fmt && cargo clippy --fix --allow-dirty --allow-staged",
162+
"lint": "prettier --write . && cargo fmt && cargo clippy --fix --allow-dirty --allow-staged && ruff format",
163163
"pretest": "npm run compile && npm run lint",
164164
"test": "node ./out/test/runTest.js",
165165
"build": "webpack --config webpack.config.js",

src/analyze.rs

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
use std::{borrow::Borrow, collections::HashMap, fmt::Debug, iter::FusedIterator, ops::ControlFlow};
55

6-
use tracing::trace;
6+
use tracing::{instrument, trace};
77
use tree_sitter::{Node, QueryCursor};
88

99
use odoo_lsp::{
@@ -146,7 +146,7 @@ impl<'a> Iterator for Iter<'a> {
146146
#[rustfmt::skip]
147147
query! {
148148
#[derive(Debug)]
149-
FieldCompletion(Name, SelfParam, Scope, Def);
149+
FieldCompletion(Name, SelfParam, Scope);
150150
((class_definition
151151
(block
152152
(expression_statement
@@ -155,8 +155,8 @@ query! {
155155
[
156156
(decorated_definition
157157
(function_definition
158-
(parameters . (identifier) @SELF_PARAM) (block) @SCOPE) .)
159-
(function_definition (parameters . (identifier) @SELF_PARAM) (block) @SCOPE)] @DEF)) @class
158+
(parameters . (identifier) @SELF_PARAM)) @SCOPE)
159+
(function_definition (parameters . (identifier) @SELF_PARAM)) @SCOPE])) @class
160160
(#match? @_name "^_(name|inherit)$"))
161161
}
162162

@@ -547,6 +547,7 @@ impl Backend {
547547
}
548548

549549
/// Returns `(self_type, fn_scope, self_param)`
550+
#[instrument(level = "trace", skip_all, ret)]
550551
pub fn determine_scope<'out, 'node>(
551552
node: Node<'node>,
552553
contents: &'out [u8],
@@ -571,19 +572,17 @@ pub fn determine_scope<'out, 'node>(
571572
Some(FieldCompletion::SelfParam) => {
572573
self_param = Some(capture.node);
573574
}
574-
Some(FieldCompletion::Def) => {
575+
Some(FieldCompletion::Scope) => {
575576
if !capture.node.byte_range().contains_end(offset) {
576577
continue 'scoping;
577578
}
578-
}
579-
Some(FieldCompletion::Scope) => {
580579
fn_scope = Some(capture.node);
581580
}
582581
None => {}
583582
}
584583
}
585584
if fn_scope.is_some() {
586-
break 'scoping;
585+
break;
587586
}
588587
}
589588
let fn_scope = fn_scope?;
@@ -617,7 +616,6 @@ class Foo(models.AbstractModel):
617616
let ast = parser.parse(&contents[..], None).unwrap();
618617
let query = FieldCompletion::query();
619618
let mut cursor = QueryCursor::new();
620-
// let expected: &[&[u32]] = &[];
621619
let actual = cursor
622620
.matches(query, ast.root_node(), &contents[..])
623621
.map(|match_| {
@@ -635,22 +633,8 @@ class Foo(models.AbstractModel):
635633
matches!(
636634
&actual[..],
637635
[
638-
[
639-
None,
640-
None,
641-
Some(T::Name),
642-
Some(T::Def),
643-
Some(T::SelfParam),
644-
Some(T::Scope)
645-
],
646-
[
647-
None,
648-
None,
649-
Some(T::Name),
650-
Some(T::Def),
651-
Some(T::SelfParam),
652-
Some(T::Scope)
653-
]
636+
[None, None, Some(T::Name), Some(T::Scope), Some(T::SelfParam)],
637+
[None, None, Some(T::Name), Some(T::Scope), Some(T::SelfParam)]
654638
]
655639
),
656640
"{actual:?}"

src/python.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use miette::{diagnostic, miette};
1212
use odoo_lsp::index::{index_models, interner, PathSymbol};
1313
use ropey::Rope;
1414
use tower_lsp::lsp_types::*;
15-
use tracing::{debug, trace, warn};
15+
use tracing::{debug, instrument, trace, warn};
1616
use tree_sitter::{Node, Parser, QueryCursor, QueryMatch, Tree};
1717

1818
use odoo_lsp::model::{ModelName, ModelType, ResolveMappedError};
@@ -551,6 +551,7 @@ impl Backend {
551551
}
552552
/// Resolves the attribute at the cursor offset.
553553
/// Returns `(object, field, range)`
554+
#[instrument(level = "trace", skip_all, ret)]
554555
pub fn attribute_node_at_offset<'out>(
555556
&'out self,
556557
mut offset: usize,
@@ -562,8 +563,10 @@ impl Backend {
562563
}
563564
offset = offset.clamp(0, contents.len() - 1);
564565
let mut cursor_node = root.descendant_for_byte_range(offset, offset)?;
566+
let mut real_offset = None;
565567
if cursor_node.is_named() && !matches!(cursor_node.kind(), "attribute" | "identifier") {
566568
// We got our cursor left in the middle of nowhere.
569+
real_offset = Some(offset);
567570
offset = offset.saturating_sub(1);
568571
cursor_node = root.descendant_for_byte_range(offset, offset)?;
569572
}
@@ -578,15 +581,12 @@ impl Backend {
578581
let rhs;
579582
if !cursor_node.is_named() {
580583
// We landed on one of the punctuations inside the attribute.
581-
// Need to determine which one is it.
584+
// Need to determine which one it is.
582585
let dot = cursor_node.descendant_for_byte_range(offset, offset)?;
583586
lhs = dot.prev_named_sibling()?;
584587
rhs = dot.next_named_sibling().and_then(|attr| match attr.kind() {
585588
"identifier" => Some(attr),
586-
// TODO: Unwrap all layers of attributes
587-
"attribute" => attr
588-
.child_by_field_name("object")
589-
.and_then(|obj| (obj.kind() == "identifier").then_some(obj)),
589+
"attribute" => attr.child_by_field_name("attribute"),
590590
_ => None,
591591
});
592592
} else if cursor_node.kind() == "attribute" {
@@ -619,13 +619,15 @@ impl Backend {
619619
let Some(rhs) = rhs else {
620620
// In single-expression mode, rhs could be empty in which case
621621
// we return an empty needle/range.
622-
return Some((lhs, Cow::from(""), offset + 1..offset + 1));
622+
let offset = real_offset.unwrap_or(offset);
623+
return Some((lhs, Cow::from(""), offset..offset));
623624
};
624625
let (field, range) = if rhs.range().start_point.row != lhs.range().end_point.row {
625626
// tree-sitter has an issue with attributes spanning multiple lines
626627
// which is NOT valid Python, but allows it anyways because tree-sitter's
627628
// use cases don't require strict syntax trees.
628-
(Cow::from(""), offset + 1..offset + 1)
629+
let offset = real_offset.unwrap_or(offset);
630+
(Cow::from(""), offset..offset)
629631
} else {
630632
let range = rhs.byte_range();
631633
(String::from_utf8_lossy(&contents[range.clone()]), range)

testing/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ def setup():
4848
)
4949
async def client(lsp_client: LanguageClient, rootdir: str):
5050
params = InitializeParams(
51-
workspace_folders=[WorkspaceFolder(uri=Path(rootdir).as_uri(), name="odoo-lsp")],
51+
workspace_folders=[
52+
WorkspaceFolder(uri=Path(rootdir).as_uri(), name="odoo-lsp")
53+
],
5254
capabilities=ClientCapabilities(
5355
window=WindowClientCapabilities(work_done_progress=True),
5456
text_document=TextDocumentClientCapabilities(

testing/fixtures/basic/bar/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ class DerivedBar(models.Model):
77
_inherit = "bar"
88

99
def test(self):
10-
pass
10+
pass

testing/fixtures/basic/foo/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ class Foo(Model):
66
def completions(self):
77
self.env["bar"]
88
# ^complete bar derived.bar foo foob
9+
for foo in self:
10+
foo.
11+
# ^complete bar
912

1013
def diagnostics(self):
1114
self.foo
@@ -17,7 +20,6 @@ def diagnostics(self):
1720
self.env["fo"]
1821
# ^diag `fo` is not a valid model name
1922

20-
2123
class Foob(Model):
2224
_name = "foob"
2325

testing/fixtures/basic/test_main.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ def __init__(self):
4242

4343
@pytest.mark.asyncio(scope="module")
4444
async def test_python(client: LanguageClient, rootdir: str):
45-
files = {file: file.read_text() for file in Path(rootdir).joinpath("foo").rglob("*.py")}
45+
files = {
46+
file: file.read_text() for file in Path(rootdir).joinpath("foo").rglob("*.py")
47+
}
4648
expected = defaultdict[Path, Expected](Expected)
4749
asts = dict[Path, Tree]()
4850
for file, text in files.items():
@@ -54,11 +56,15 @@ async def test_python(client: LanguageClient, rootdir: str):
5456
text = text.decode()
5557
if (offset := text.find("^diag ")) != -1:
5658
msg = text[offset + 6 :].strip()
57-
pos = Position(node.start_point.row - 1, node.start_point.column + offset)
59+
pos = Position(
60+
node.start_point.row - 1, node.start_point.column + offset
61+
)
5862
expected[file].diag.append((pos, msg))
5963
elif (offset := text.find("^complete ")) != -1:
6064
completions = text[offset + 10 :].strip().split(" ")
61-
pos = Position(node.start_point.row - 1, node.start_point.column + offset)
65+
pos = Position(
66+
node.start_point.row - 1, node.start_point.column + offset
67+
)
6268
expected[file].complete.append((pos, completions))
6369

6470
unexpected: list[str] = []
@@ -81,7 +87,9 @@ async def test_python(client: LanguageClient, rootdir: str):
8187
for missing in diff.pop("iterable_item_removed", {}).values(): # type: ignore
8288
unexpected.append(f"diag: missing {missing}\n at {file}")
8389
for mismatch in diff.pop("values_changed", {}).values(): # type: ignore
84-
unexpected.append(f"diag: expected={mismatch['old_value']!r} actual={mismatch['new_value']!r}\n at {file}")
90+
unexpected.append(
91+
f"diag: expected={mismatch['old_value']!r} actual={mismatch['new_value']!r}\n at {file}"
92+
)
8593
if diff:
8694
unexpected.append(f"diag: unexpected {diff}\n at {file}")
8795

@@ -95,13 +103,17 @@ async def test_python(client: LanguageClient, rootdir: str):
95103
assert isinstance(results, CompletionList)
96104
actual = [e.label for e in results.items]
97105
if actual != expected_completion:
98-
node = asts[file].root_node.named_descendant_for_point_range((pos.line, pos.character), (pos.line, 9999))
106+
node = asts[file].root_node.named_descendant_for_point_range(
107+
(pos.line, pos.character), (pos.line, 9999)
108+
)
99109
assert node
100110
if text := node.text:
101111
node_text = text.decode()
102112
else:
103113
node_text = ""
104-
unexpected.append(f"complete: actual={' '.join(actual)}\n at {file}:{pos}\n{' ' * node.start_point.column}{node_text}")
114+
unexpected.append(
115+
f"complete: actual={' '.join(actual)}\n at {file}:{pos}\n{' ' * node.start_point.column}{node_text}"
116+
)
105117
unexpected_len = len(unexpected)
106118
assert not unexpected_len, "\n".join(unexpected)
107119

0 commit comments

Comments
 (0)