Skip to content

Commit d1457d6

Browse files
committed
feat(lsp): snippet style completion for commands
1 parent 7ca2a6f commit d1457d6

File tree

1 file changed

+78
-38
lines changed

1 file changed

+78
-38
lines changed

crates/nu-lsp/src/completion.rs

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ use std::sync::Arc;
33
use crate::{span_to_range, uri_to_path, LanguageServer};
44
use lsp_types::{
55
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams,
6-
CompletionResponse, CompletionTextEdit, Documentation, MarkupContent, MarkupKind, TextEdit,
6+
CompletionResponse, CompletionTextEdit, Documentation, InsertTextFormat, MarkupContent,
7+
MarkupKind, TextEdit,
78
};
89
use nu_cli::{NuCompleter, SuggestionKind};
910
use nu_protocol::{
1011
engine::{CommandType, Stack},
11-
Span,
12+
PositionalArg, Span, SyntaxShape,
1213
};
1314

1415
impl LanguageServer {
@@ -45,27 +46,69 @@ impl LanguageServer {
4546
results
4647
.into_iter()
4748
.map(|r| {
48-
let decl_id = r.kind.clone().and_then(|kind| {
49+
let decl_id = r.kind.as_ref().and_then(|kind| {
4950
matches!(kind, SuggestionKind::Command(_))
5051
.then_some(engine_state.find_decl(r.suggestion.value.as_bytes(), &[])?)
5152
});
5253

53-
let mut label_value = r.suggestion.value;
54-
if r.suggestion.append_whitespace {
55-
label_value.push(' ');
54+
let mut snippet_text = r.suggestion.value.clone();
55+
let mut doc_string = r.suggestion.extra.map(|ex| ex.join("\n"));
56+
let mut insert_text_format = None;
57+
let mut idx = 1;
58+
// use snippet as `insert_text_format` for command argument completion
59+
if let Some(decl_id) = decl_id {
60+
let cmd = engine_state.get_decl(decl_id);
61+
doc_string = Some(Self::get_decl_description(cmd, true));
62+
insert_text_format = Some(InsertTextFormat::SNIPPET);
63+
64+
let signature = cmd.signature();
65+
// add curly brackets around block arguments
66+
let block_wrapper = |arg: &PositionalArg, text: String| -> String {
67+
if matches!(arg.shape, SyntaxShape::Block | SyntaxShape::MatchBlock) {
68+
format!("{{{text}}}")
69+
} else {
70+
text
71+
}
72+
};
73+
74+
for required in signature.required_positional {
75+
snippet_text.push(' ');
76+
snippet_text.push_str(
77+
block_wrapper(&required, format!("${{{}:{}}}", idx, required.name))
78+
.as_str(),
79+
);
80+
idx += 1;
81+
}
82+
for optional in signature.optional_positional {
83+
snippet_text.push(' ');
84+
snippet_text.push_str(
85+
block_wrapper(&optional, format!("${{{}:{}}}", idx, optional.name))
86+
.as_str(),
87+
);
88+
idx += 1;
89+
}
90+
if let Some(rest) = signature.rest_positional {
91+
snippet_text
92+
.push_str(format!(" ${{{}:...{}}}", idx, rest.name).as_str());
93+
idx += 1;
94+
}
95+
}
96+
// no extra space for a command with args in the snippet
97+
if idx == 1 && r.suggestion.append_whitespace {
98+
snippet_text.push(' ');
5699
}
57100

58101
let span = r.suggestion.span;
59102
let text_edit = Some(CompletionTextEdit::Edit(TextEdit {
60103
range: span_to_range(&Span::new(span.start, span.end), file, 0),
61-
new_text: label_value.clone(),
104+
new_text: snippet_text,
62105
}));
63106

64107
CompletionItem {
65-
label: label_value,
108+
label: r.suggestion.value,
66109
label_details: r
67110
.kind
68-
.clone()
111+
.as_ref()
69112
.map(|kind| match kind {
70113
SuggestionKind::Value(t) => t.to_string(),
71114
SuggestionKind::Command(cmd) => cmd.to_string(),
@@ -80,21 +123,15 @@ impl LanguageServer {
80123
description: Some(s),
81124
}),
82125
detail: r.suggestion.description,
83-
documentation: r
84-
.suggestion
85-
.extra
86-
.map(|ex| ex.join("\n"))
87-
.or(decl_id.map(|decl_id| {
88-
Self::get_decl_description(engine_state.get_decl(decl_id), true)
89-
}))
90-
.map(|value| {
91-
Documentation::MarkupContent(MarkupContent {
92-
kind: MarkupKind::Markdown,
93-
value,
94-
})
95-
}),
126+
documentation: doc_string.map(|value| {
127+
Documentation::MarkupContent(MarkupContent {
128+
kind: MarkupKind::Markdown,
129+
value,
130+
})
131+
}),
96132
kind: Self::lsp_completion_item_kind(r.kind),
97133
text_edit,
134+
insert_text_format,
98135
..Default::default()
99136
}
100137
})
@@ -221,9 +258,9 @@ mod tests {
221258
actual: result_from_message(resp),
222259
expected: serde_json::json!([
223260
// defined after the cursor
224-
{ "label": "config n foo bar ", "detail": detail_str, "kind": 2 },
261+
{ "label": "config n foo bar", "detail": detail_str, "kind": 2 },
225262
{
226-
"label": "config nu ",
263+
"label": "config nu",
227264
"detail": "Edit nu configurations.",
228265
"textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
229266
"newText": "config nu "
@@ -236,7 +273,7 @@ mod tests {
236273
let resp = send_complete_request(&client_connection, script.clone(), 1, 18);
237274
assert!(result_from_message(resp).as_array().unwrap().contains(
238275
&serde_json::json!({
239-
"label": "-s ",
276+
"label": "-s",
240277
"detail": "test flag",
241278
"labelDetails": { "description": "flag" },
242279
"textEdit": { "range": { "start": { "line": 1, "character": 17 }, "end": { "line": 1, "character": 18 }, },
@@ -250,7 +287,7 @@ mod tests {
250287
let resp = send_complete_request(&client_connection, script.clone(), 2, 22);
251288
assert!(result_from_message(resp).as_array().unwrap().contains(
252289
&serde_json::json!({
253-
"label": "--long ",
290+
"label": "--long",
254291
"detail": "test flag",
255292
"labelDetails": { "description": "flag" },
256293
"textEdit": { "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 22 }, },
@@ -277,7 +314,7 @@ mod tests {
277314
let resp = send_complete_request(&client_connection, script, 10, 34);
278315
assert!(result_from_message(resp).as_array().unwrap().contains(
279316
&serde_json::json!({
280-
"label": "-g ",
317+
"label": "-g",
281318
"detail": "count indexes and split using grapheme clusters (all visible chars have length 1)",
282319
"labelDetails": { "description": "flag" },
283320
"textEdit": { "range": { "start": { "line": 10, "character": 33 }, "end": { "line": 10, "character": 34 }, },
@@ -305,13 +342,14 @@ mod tests {
305342
actual: result_from_message(resp),
306343
expected: serde_json::json!([
307344
{
308-
"label": "alias ",
345+
"label": "alias",
309346
"labelDetails": { "description": "keyword" },
310347
"detail": "Alias a command (with optional flags) to a new name.",
311348
"textEdit": {
312349
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, },
313-
"newText": "alias "
350+
"newText": "alias ${1:name} ${2:initial_value}"
314351
},
352+
"insertTextFormat": 2,
315353
"kind": 14
316354
}
317355
])
@@ -322,13 +360,14 @@ mod tests {
322360
actual: result_from_message(resp),
323361
expected: serde_json::json!([
324362
{
325-
"label": "alias ",
363+
"label": "alias",
326364
"labelDetails": { "description": "keyword" },
327365
"detail": "Alias a command (with optional flags) to a new name.",
328366
"textEdit": {
329367
"range": { "start": { "line": 3, "character": 2 }, "end": { "line": 3, "character": 2 }, },
330-
"newText": "alias "
368+
"newText": "alias ${1:name} ${2:initial_value}"
331369
},
370+
"insertTextFormat": 2,
332371
"kind": 14
333372
}
334373
])
@@ -364,13 +403,14 @@ mod tests {
364403
actual: result_from_message(resp),
365404
expected: serde_json::json!([
366405
{
367-
"label": "str trim ",
406+
"label": "str trim",
368407
"labelDetails": { "description": "built-in" },
369408
"detail": "Trim whitespace or specific character.",
370409
"textEdit": {
371410
"range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 }, },
372-
"newText": "str trim "
411+
"newText": "str trim ${1:...rest}"
373412
},
413+
"insertTextFormat": 2,
374414
"kind": 3
375415
}
376416
])
@@ -394,7 +434,7 @@ mod tests {
394434
actual: result_from_message(resp),
395435
expected: serde_json::json!([
396436
{
397-
"label": "overlay ",
437+
"label": "overlay",
398438
"labelDetails": { "description": "keyword" },
399439
"textEdit": {
400440
"newText": "overlay ",
@@ -483,12 +523,12 @@ mod tests {
483523
actual: result_from_message(resp),
484524
expected: serde_json::json!([
485525
{
486-
"label": "alias ",
526+
"label": "alias",
487527
"labelDetails": { "description": "keyword" },
488528
"detail": "Alias a command (with optional flags) to a new name.",
489529
"textEdit": {
490530
"range": { "start": { "line": 0, "character": 5 }, "end": { "line": 0, "character": 5 }, },
491-
"newText": "alias "
531+
"newText": "alias ${1:name} ${2:initial_value}"
492532
},
493533
"kind": 14
494534
},
@@ -513,7 +553,7 @@ mod tests {
513553
actual: result_from_message(resp),
514554
expected: serde_json::json!([
515555
{
516-
"label": "!= ",
556+
"label": "!=",
517557
"labelDetails": { "description": "operator" },
518558
"textEdit": {
519559
"newText": "!= ",
@@ -529,7 +569,7 @@ mod tests {
529569
actual: result_from_message(resp),
530570
expected: serde_json::json!([
531571
{
532-
"label": "not-has ",
572+
"label": "not-has",
533573
"labelDetails": { "description": "operator" },
534574
"textEdit": {
535575
"newText": "not-has ",

0 commit comments

Comments
 (0)