Skip to content

Commit

Permalink
fix: support incomplete attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
Desdaemon committed May 20, 2024
1 parent 8eaa51f commit 70af361
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 43 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 9 additions & 25 deletions src/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
Expand All @@ -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)$"))
}

Expand Down Expand Up @@ -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],
Expand All @@ -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?;
Expand Down Expand Up @@ -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_| {
Expand All @@ -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:?}"
Expand Down
18 changes: 10 additions & 8 deletions src/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand All @@ -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)?;
}
Expand All @@ -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" {
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion testing/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion testing/fixtures/basic/bar/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ class DerivedBar(models.Model):
_inherit = "bar"

def test(self):
pass
pass
4 changes: 3 additions & 1 deletion testing/fixtures/basic/foo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,7 +20,6 @@ def diagnostics(self):
self.env["fo"]
# ^diag `fo` is not a valid model name


class Foob(Model):
_name = "foob"

Expand Down
24 changes: 18 additions & 6 deletions testing/fixtures/basic/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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] = []
Expand All @@ -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}")

Expand All @@ -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)

Expand Down

0 comments on commit 70af361

Please sign in to comment.