commit f3b29c1b0f85b44ab148dcd044784f31ddb9ed03
parent 844350bac1840dd4a7eb575ced064e8eec2ee55b
Author: Sylvia Ivory <git@sivory.net>
Date: Tue, 24 Jun 2025 00:42:27 -0700
Finish local resolver
Diffstat:
2 files changed, 126 insertions(+), 34 deletions(-)
diff --git a/queries/locals.scm b/queries/locals.scm
@@ -0,0 +1,35 @@
+; Scopes
+
+[
+ (chunk)
+ (do_statement)
+ (while_statement)
+ (repeat_statement)
+ (if_statement)
+ (for_statement)
+ (function_definition)
+ (function_declaration)
+] @local.scope
+
+; Definitions
+
+(assignment_statement
+ (variable_list
+ (identifier) @local.definition))
+
+; technically a "local function name() ... end" is
+; local name
+; name = function() ... end
+; so we need to ensure that the function name goes into the parent scope
+; to do what we treat it as a special case and hoist it in the code
+(function_declaration
+ name: (identifier) @local.fn_name)
+
+(for_generic_clause
+ (variable_list
+ (identifier) @local.definition))
+
+(for_numeric_clause
+ name: (identifier) @local.definition)
+
+(parameters (identifier) @local.definition)
diff --git a/src/completion.rs b/src/completion.rs
@@ -1,13 +1,16 @@
-use std::collections::HashSet;
-
use mlua::prelude::*;
-use tree_sitter::{Parser, Point, Query, QueryCursor, StreamingIterator, Tree};
+use tree_sitter::{Parser, Point, Query, QueryCursor, Range, StreamingIterator, Tree};
+
+#[derive(Debug)]
+struct Variable {
+ range: Range,
+ name: String,
+}
#[derive(Debug)]
struct Scope {
- start: Point,
- end: Point,
- variables: HashSet<String>,
+ range: Range,
+ variables: Vec<Variable>,
}
pub struct LuaCompleter {
@@ -16,6 +19,7 @@ pub struct LuaCompleter {
tree: Tree,
locals_query: Query,
+ scopes: Vec<Scope>,
text: String,
}
@@ -31,7 +35,7 @@ impl LuaCompleter {
let locals_query = Query::new(
&tree_sitter_lua::LANGUAGE.into(),
- tree_sitter_lua::LOCALS_QUERY,
+ include_str!("../queries/locals.scm"),
)
.unwrap();
@@ -40,6 +44,7 @@ impl LuaCompleter {
parser,
tree,
locals_query,
+ scopes: Vec::new(),
text: String::new(),
}
}
@@ -47,6 +52,7 @@ impl LuaCompleter {
fn refresh_tree(&mut self, text: &str) {
self.tree = self.parser.parse(text, None).unwrap();
self.text = text.to_string();
+ self.scopes = self.resolve_scopes();
}
fn globals(&self) -> Vec<LuaValue> {
@@ -58,7 +64,7 @@ impl LuaCompleter {
.collect()
}
- fn locals(&self, point: Point) -> Vec<String> {
+ fn resolve_scopes(&self) -> Vec<Scope> {
let mut cursor = QueryCursor::new();
let matches = cursor.matches(
@@ -68,48 +74,75 @@ impl LuaCompleter {
);
let names = self.locals_query.capture_names();
- let mut scopes = Vec::new();
- let mut variables = Vec::new();
+ let mut scopes: Vec<Scope> = Vec::new();
+ let mut scope_hierarchy: Vec<usize> = Vec::new();
matches.for_each(|m| {
for capture in m.captures {
let name = names[capture.index as usize];
+ let text = capture
+ .node
+ .utf8_text(self.text.as_bytes())
+ .unwrap()
+ .to_string();
+ let range = capture.node.range();
match name {
"local.definition" => {
- variables.push((
- capture.node.start_position(),
- capture.node.utf8_text(self.text.as_bytes()).unwrap(),
- ));
+ let last = scope_hierarchy.last().unwrap();
+ let last_scope = &mut scopes[*last];
+
+ last_scope.variables.push(Variable { range, name: text });
+ }
+ "local.fn_name" => {
+ let len = scope_hierarchy.len();
+ let parent = scope_hierarchy
+ .get(len - 2)
+ .unwrap_or_else(|| scope_hierarchy.last().unwrap());
+ let scope = &mut scopes[*parent];
+
+ scope.variables.push(Variable { range, name: text });
}
"local.scope" => {
- scopes.push((capture.node.start_position(), capture.node.end_position()));
+ let scope = Scope {
+ range: capture.node.range(),
+ variables: Vec::new(),
+ };
+
+ if let Some(last) = scope_hierarchy.last() {
+ // outside
+ let last_scope = &scopes[*last];
+
+ if scope.range.end_byte > last_scope.range.end_byte {
+ scope_hierarchy.pop();
+ }
+ }
+
+ scope_hierarchy.push(scopes.len());
+ scopes.push(scope);
}
_ => {}
}
}
});
- let new_scopes = scopes
- .into_iter()
- .map(|(start, end)| {
- let variables = variables
- .iter()
- .filter(|v| v.0 >= start && v.0 <= end)
- .map(|v| v.1.to_string())
- .collect::<HashSet<_>>();
-
- Scope {
- start,
- end,
- variables,
- }
- })
- .collect::<Vec<_>>();
+ scopes
+ }
+
+ fn locals(&self, point: Point) -> Vec<String> {
+ let mut variables = Vec::new();
- dbg!(&new_scopes);
+ for scope in self.scopes.iter() {
+ if point > scope.range.start_point && point < scope.range.end_point {
+ for var in scope.variables.iter() {
+ if point > var.range.end_point {
+ variables.push(var.name.clone());
+ }
+ }
+ }
+ }
- Vec::new()
+ variables
}
}
@@ -124,15 +157,39 @@ mod tests {
completer.refresh_tree(
r#"
local function foo(a, b)
+ -- 2: foo, a, b
print(a, b)
end
+ -- 6: foo
+
local function bar(c)
+ -- 9: foo, bar, c
print(c)
end
+
+ -- 13: foo, bar
"#,
);
- completer.locals(Point { row: 1, column: 0 });
+ assert_eq!(
+ &["foo", "a", "b"].as_slice(),
+ &completer.locals(Point { row: 2, column: 0 }),
+ );
+
+ assert_eq!(
+ &["foo"].as_slice(),
+ &completer.locals(Point { row: 6, column: 0 }),
+ );
+
+ assert_eq!(
+ &["foo", "bar", "c"].as_slice(),
+ &completer.locals(Point { row: 9, column: 0 }),
+ );
+
+ assert_eq!(
+ &["foo", "bar"].as_slice(),
+ &completer.locals(Point { row: 13, column: 0 }),
+ );
}
}