Skip to content

Commit 000e28f

Browse files
committed
feat: Improve completions code
1 parent 146f5c1 commit 000e28f

File tree

3 files changed

+162
-106
lines changed

3 files changed

+162
-106
lines changed

src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ pub fn index(paths: Option<&[PathBuf]>, print_items: bool) -> Result<(), DebugEr
431431
Ok(())
432432
}
433433

434-
pub fn print_tree(path: &Path) -> Result<(), DebugError> {
434+
pub fn ast(path: &Path) -> Result<(), DebugError> {
435435
let text = match std::fs::read_to_string(path) {
436436
Ok(text) => text,
437437
Err(err) => {

src/completions.rs

Lines changed: 159 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,8 @@ pub fn get(
1515
tree: &tree_sitter::Tree,
1616
symbols_map: &impl SymbolsMap,
1717
) -> Option<CompletionResponse> {
18-
let line = rope.get_line(position.line as usize)?;
19-
let mut query = String::new();
20-
for (i, char) in line.chars().enumerate() {
21-
if char.is_alphabetic() || char == '.' || (!query.is_empty() && char.is_numeric()) {
22-
query.push(char)
23-
} else {
24-
query.clear();
25-
}
26-
if i == (position.character - 1) as usize {
27-
break;
28-
}
29-
}
18+
let query = extract_query(position, rope)?;
19+
3020
tracing::debug!("completion query: {query}");
3121

3222
let symbol_kind_to_completion_kind = |kind: SymbolKind| match kind {
@@ -36,61 +26,67 @@ pub fn get(
3626
_ => CompletionItemKind::VARIABLE,
3727
};
3828

39-
let point = Point {
40-
row: position.line as usize,
41-
column: position.character as usize,
42-
};
29+
let local_symbols: Vec<CompletionItem> = {
30+
let point = Point {
31+
row: position.line as usize,
32+
column: position.character as usize,
33+
};
34+
tree.root_node()
35+
.descendant_for_point_range(point, point)
36+
.map(|node| {
37+
std::iter::successors(Some(node), |node| node.parent())
38+
.filter(|node| node.kind() == "function_definition")
39+
.flat_map(|func_node| {
40+
let mut items = Vec::new();
4341

44-
// TODO: filter for query
45-
let local_symbols: Vec<CompletionItem> = tree
46-
.root_node()
47-
.descendant_for_point_range(point, point)
48-
.map(|node| {
49-
std::iter::successors(Some(node), |node| node.parent())
50-
.filter(|n| n.kind() == "function_definition")
51-
.flat_map(|func_node| {
52-
let mut items = Vec::new();
53-
54-
if let Some(parameters) = func_node.child_by_field_name("parameters") {
55-
items.extend(
56-
parameters
57-
.children_by_field_name("parameter", &mut parameters.walk())
58-
.filter_map(|parameter| {
59-
parameter.child_by_field_name("name").map(|name| {
60-
CompletionItem {
61-
label: rope
62-
.byte_slice(name.start_byte()..name.end_byte())
63-
.to_string(),
64-
kind: Some(CompletionItemKind::VARIABLE),
65-
..Default::default()
66-
}
42+
if let Some(parameters) = func_node.child_by_field_name("parameters") {
43+
items.extend(
44+
parameters
45+
.children_by_field_name("parameter", &mut parameters.walk())
46+
.filter_map(|parameter| {
47+
parameter.child_by_field_name("name").map(|name| {
48+
rope.byte_slice(name.start_byte()..name.end_byte())
49+
.to_string()
50+
})
6751
})
68-
}),
69-
);
70-
}
52+
.filter(|name| utils::starts_with_lowercase(name, &query))
53+
.map(|label| CompletionItem {
54+
label,
55+
kind: Some(CompletionItemKind::VARIABLE),
56+
..Default::default()
57+
}),
58+
);
59+
}
7160

72-
if let Some(body) = func_node.child_by_field_name("body") {
73-
items.extend(index::symbols_for_block(body, rope).into_iter().map(
74-
|symbol| CompletionItem {
75-
label: symbol.name,
76-
kind: Some(symbol_kind_to_completion_kind(symbol.kind)),
77-
..Default::default()
78-
},
79-
));
80-
}
61+
if let Some(body) = func_node.child_by_field_name("body") {
62+
items.extend(
63+
index::symbols_for_block(body, rope)
64+
.into_iter()
65+
.filter(|symbol| {
66+
utils::starts_with_lowercase(&symbol.name, &query)
67+
})
68+
.map(|symbol| CompletionItem {
69+
label: symbol.name,
70+
kind: Some(symbol_kind_to_completion_kind(symbol.kind)),
71+
..Default::default()
72+
}),
73+
);
74+
}
8175

82-
items
83-
})
84-
.collect()
85-
})
86-
.unwrap_or_default();
76+
items
77+
})
78+
.collect()
79+
})
80+
.unwrap_or_default()
81+
};
8782

8883
let workspace_symbols = symbols_map.filter_map(
8984
|_, symbols| {
9085
symbols
9186
.iter()
9287
.filter(|symbol| utils::starts_with_lowercase(&symbol.name, &query))
9388
},
89+
// TODO: use CompletionResponse::List.is_incomplete and only limit for short queries?
9490
1024,
9591
);
9692

@@ -119,7 +115,7 @@ pub fn get(
119115
Some(CompletionResponse::Array(
120116
RESERVED_WORDS
121117
.iter()
122-
// TODO: filter keywords
118+
.filter(|keyword| utils::starts_with_lowercase(keyword, &query))
123119
.map(|reserved_word| CompletionItem {
124120
label: reserved_word.to_string(),
125121
kind: Some(CompletionItemKind::KEYWORD),
@@ -135,77 +131,137 @@ pub fn get(
135131
))
136132
}
137133

134+
// TODO: consider throwing error instead of optional
135+
// TODO: alternatively use tree-sitter to extract nearest identifer (to avoid reimplementing nearest identifer)
136+
fn extract_query(position: Position, rope: &Rope) -> Option<String> {
137+
let line = rope.get_line(position.line as usize)?;
138+
Some(
139+
line.chars()
140+
.take(position.character as usize)
141+
.fold(String::new(), |mut acc, item| {
142+
// see: https://cran.r-project.org/doc/manuals/r-release/R-lang.html#Identifiers-1
143+
// note: we can be less strict than R otherwise its already an parser
144+
if item.is_alphabetic()
145+
|| item == '.'
146+
|| item == '_'
147+
|| (!acc.is_empty() && item.is_numeric())
148+
{
149+
acc.push(item);
150+
} else {
151+
acc.clear();
152+
}
153+
acc
154+
}),
155+
)
156+
}
157+
138158
#[cfg(test)]
139159
mod tests {
140-
use {
141-
super::*,
142-
crate::tree,
143-
async_lsp::lsp_types::{CompletionItemKind, Position},
144-
indoc::indoc,
145-
ropey::Rope,
146-
};
160+
use {super::*, crate::tree, indoc::indoc, ropey::Rope, std::collections::HashMap};
147161

148-
fn get_labels(
162+
fn setup(
149163
text: &str,
150-
line: u32,
151-
character: u32,
152-
) -> Vec<(String, Option<CompletionItemKind>)> {
164+
(line, character): (u32, u32),
165+
) -> (String, Vec<(String, CompletionItemKind)>) {
153166
let rope = Rope::from_str(text);
154167
let tree = tree::parse(text, None);
155-
let dummy_symbols = std::collections::HashMap::new();
156-
let result = get(Position { line, character }, &rope, &tree, &dummy_symbols).unwrap();
157-
match result {
158-
async_lsp::lsp_types::CompletionResponse::Array(items) => items
168+
let position = Position::new(line, character);
169+
let query = extract_query(position, &rope).unwrap();
170+
let items = match get(position, &rope, &tree, &HashMap::new()).unwrap() {
171+
CompletionResponse::Array(items) => items
159172
.into_iter()
160-
.map(|item| (item.label, item.kind))
173+
.map(|item| (item.label, item.kind.unwrap()))
161174
.collect(),
162175
_ => unreachable!(),
163-
}
176+
};
177+
(query, items)
164178
}
165179

166180
#[test]
167181
fn completes_local_variables() {
168-
let text = indoc! {"
169-
foo <- function(x) {
170-
local_var <- 42
171-
loc
172-
}
173-
"};
174-
175-
let labels = get_labels(text, 2, 7);
182+
let (query, items) = setup(
183+
indoc! {"
184+
function(x) {
185+
local_var <- 42
186+
loc
187+
}
188+
"},
189+
(2, 7),
190+
);
176191

177-
assert!(labels.contains(&("local_var".into(), Some(CompletionItemKind::VARIABLE))));
192+
assert_eq!(query, "loc");
193+
assert_eq!(items.len(), 1);
194+
assert!(items.contains(&("local_var".into(), CompletionItemKind::VARIABLE)));
178195
}
179196

180197
#[test]
181198
fn completes_function_parameters() {
182-
let text = indoc! {"
183-
foo <- function(param1, param2) {
184-
par
185-
}
186-
"};
187-
188-
let labels = get_labels(text, 1, 7);
199+
let (query, items) = setup(
200+
indoc! {"
201+
function(param1, param2) {
202+
par
203+
}
204+
"},
205+
(1, 7),
206+
);
189207

190-
assert!(labels.contains(&("param1".into(), Some(CompletionItemKind::VARIABLE))));
191-
assert!(labels.contains(&("param2".into(), Some(CompletionItemKind::VARIABLE))));
208+
assert_eq!(query, "par");
209+
assert_eq!(items.len(), 2);
210+
assert!(items.contains(&("param1".into(), CompletionItemKind::VARIABLE)));
211+
assert!(items.contains(&("param2".into(), CompletionItemKind::VARIABLE)));
192212
}
193213

194214
#[test]
195215
fn completes_nested_function_variable() {
196-
let text = indoc! {"
197-
foo <- function(x) {
198-
var_outer <- 1
199-
bar <- function(y) {
200-
var_inner <- 2
201-
var
216+
let (query, items) = setup(
217+
indoc! {"
218+
function(x) {
219+
var_a <- 1
220+
function(y) {
221+
var_b <- 2
222+
function(y) {
223+
var_c <- 3
224+
var
225+
}
226+
}
202227
}
203-
}
204-
"};
228+
"},
229+
(6, 15),
230+
);
231+
232+
assert_eq!(query, "var");
233+
assert_eq!(items.len(), 3);
234+
assert!(items.contains(&("var_a".into(), CompletionItemKind::VARIABLE)));
235+
assert!(items.contains(&("var_b".into(), CompletionItemKind::VARIABLE)));
236+
assert!(items.contains(&("var_c".into(), CompletionItemKind::VARIABLE)));
237+
}
205238

206-
let labels = get_labels(text, 4, 11);
239+
#[test]
240+
fn completes_keywords() {
241+
let (query, items) = setup("i", (0, 1));
242+
243+
assert_eq!(query, "i");
244+
assert_eq!(items.len(), 3);
245+
assert!(items.contains(&("if".into(), CompletionItemKind::KEYWORD)));
246+
assert!(items.contains(&("in".into(), CompletionItemKind::KEYWORD)));
247+
assert!(items.contains(&("Inf".into(), CompletionItemKind::KEYWORD)));
248+
249+
let (query, items) = setup("na_ ", (0, 3));
250+
251+
assert_eq!(query, "na_");
252+
assert_eq!(items.len(), 4);
253+
}
254+
255+
#[test]
256+
fn extract_query_edge_cases() {
257+
fn setup(pos: u32, text: &str) -> String {
258+
extract_query(Position::new(0, pos), &Rope::from_str(text)).unwrap()
259+
}
207260

208-
assert!(labels.contains(&("var_inner".into(), Some(CompletionItemKind::VARIABLE))));
209-
assert!(labels.contains(&("var_outer".into(), Some(CompletionItemKind::VARIABLE))));
261+
assert_eq!(setup(11, "foo.bar_123"), "foo.bar_123");
262+
assert_eq!(setup(4, ".foo"), ".foo");
263+
assert_eq!(setup(4, "1foo"), "foo");
264+
assert_eq!(setup(4, "_foo"), "_foo");
265+
assert_eq!(setup(5, ".1foo"), ".1foo");
210266
}
211267
}

src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fn main() -> ExitCode {
3838
Ok(()) => ExitCode::SUCCESS,
3939
Err(DebugError) => ExitCode::FAILURE,
4040
},
41-
Debug::PrintTree { path } => match cli::print_tree(&path) {
41+
Debug::Ast { path } => match cli::ast(&path) {
4242
Ok(()) => ExitCode::SUCCESS,
4343
Err(DebugError) => ExitCode::FAILURE,
4444
},
@@ -94,7 +94,7 @@ enum Command {
9494
#[derive(Debug, Subcommand)]
9595
enum Debug {
9696
/// Print the syntax tree for the given file
97-
PrintTree { path: PathBuf },
97+
Ast { path: PathBuf },
9898
/// Index the given files or directories
9999
Index {
100100
/// R files to index

0 commit comments

Comments
 (0)