@@ -3,12 +3,13 @@ use std::sync::Arc;
33use crate :: { span_to_range, uri_to_path, LanguageServer } ;
44use lsp_types:: {
55 CompletionItem , CompletionItemKind , CompletionItemLabelDetails , CompletionParams ,
6- CompletionResponse , CompletionTextEdit , Documentation , MarkupContent , MarkupKind , TextEdit ,
6+ CompletionResponse , CompletionTextEdit , Documentation , InsertTextFormat , MarkupContent ,
7+ MarkupKind , TextEdit ,
78} ;
89use nu_cli:: { NuCompleter , SuggestionKind } ;
910use nu_protocol:: {
1011 engine:: { CommandType , Stack } ,
11- Span ,
12+ PositionalArg , Span , SyntaxShape ,
1213} ;
1314
1415impl 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