mirror of
https://github.com/nushell/nushell.git
synced 2025-04-29 15:44:28 +02:00
# Description Fixes some leftover issues for keyword snippets of #15494 # Tests + Formatting Adjusted
700 lines
27 KiB
Rust
700 lines
27 KiB
Rust
use std::sync::Arc;
|
|
|
|
use crate::{span_to_range, uri_to_path, LanguageServer};
|
|
use lsp_types::{
|
|
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams,
|
|
CompletionResponse, CompletionTextEdit, Documentation, InsertTextFormat, MarkupContent,
|
|
MarkupKind, Range, TextEdit,
|
|
};
|
|
use nu_cli::{NuCompleter, SemanticSuggestion, SuggestionKind};
|
|
use nu_protocol::{
|
|
engine::{CommandType, EngineState, Stack},
|
|
PositionalArg, Span, SyntaxShape,
|
|
};
|
|
|
|
impl LanguageServer {
|
|
pub(crate) fn complete(&mut self, params: &CompletionParams) -> Option<CompletionResponse> {
|
|
let path_uri = params.text_document_position.text_document.uri.to_owned();
|
|
let docs = self.docs.lock().ok()?;
|
|
let file = docs.get_document(&path_uri)?;
|
|
let location = file.offset_at(params.text_document_position.position) as usize;
|
|
let file_text = file.get_content(None).to_owned();
|
|
drop(docs);
|
|
// fallback to default completer where
|
|
// the text is truncated to `location` and
|
|
// an extra placeholder token is inserted for correct parsing
|
|
let need_fallback = location == 0
|
|
|| file_text
|
|
.get(location - 1..location)
|
|
.and_then(|s| s.chars().next())
|
|
.is_some_and(|c| c.is_whitespace() || "|(){}[]<>,:;".contains(c));
|
|
|
|
self.need_parse |= need_fallback;
|
|
let engine_state = Arc::new(self.new_engine_state(Some(&path_uri)));
|
|
let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new()));
|
|
let results = if need_fallback {
|
|
completer.fetch_completions_at(&file_text[..location], location)
|
|
} else {
|
|
let file_path = uri_to_path(&path_uri);
|
|
let filename = file_path.to_str()?;
|
|
completer.fetch_completions_within_file(filename, location, &file_text)
|
|
};
|
|
|
|
let docs = self.docs.lock().ok()?;
|
|
let file = docs.get_document(&path_uri)?;
|
|
(!results.is_empty()).then_some(CompletionResponse::Array(
|
|
results
|
|
.into_iter()
|
|
.map(|r| {
|
|
let reedline_span = r.suggestion.span;
|
|
Self::completion_item_from_suggestion(
|
|
&engine_state,
|
|
r,
|
|
span_to_range(&Span::new(reedline_span.start, reedline_span.end), file, 0),
|
|
)
|
|
})
|
|
.collect(),
|
|
))
|
|
}
|
|
|
|
fn completion_item_from_suggestion(
|
|
engine_state: &EngineState,
|
|
suggestion: SemanticSuggestion,
|
|
range: Range,
|
|
) -> CompletionItem {
|
|
let decl_id = suggestion.kind.as_ref().and_then(|kind| {
|
|
matches!(kind, SuggestionKind::Command(_))
|
|
.then_some(engine_state.find_decl(suggestion.suggestion.value.as_bytes(), &[])?)
|
|
});
|
|
|
|
let mut snippet_text = suggestion.suggestion.value.clone();
|
|
let mut doc_string = suggestion.suggestion.extra.map(|ex| ex.join("\n"));
|
|
let mut insert_text_format = None;
|
|
let mut idx = 0;
|
|
// use snippet as `insert_text_format` for command argument completion
|
|
if let Some(decl_id) = decl_id {
|
|
let cmd = engine_state.get_decl(decl_id);
|
|
doc_string = Some(Self::get_decl_description(cmd, true));
|
|
insert_text_format = Some(InsertTextFormat::SNIPPET);
|
|
let signature = cmd.signature();
|
|
// add curly brackets around block arguments
|
|
// and keywords, e.g. `=` in `alias foo = bar`
|
|
let mut arg_wrapper = |arg: &PositionalArg, text: String, optional: bool| -> String {
|
|
idx += 1;
|
|
match &arg.shape {
|
|
SyntaxShape::Block | SyntaxShape::MatchBlock => {
|
|
format!("{{ ${{{}:{}}} }}", idx, text)
|
|
}
|
|
SyntaxShape::Keyword(kwd, _) => {
|
|
// NOTE: If optional, the keyword should also be in a placeholder so that it can be removed easily.
|
|
// Here we choose to use nested placeholders. Note that some editors don't fully support this format,
|
|
// but usually they will simply ignore the inner ones, so it should be fine.
|
|
if optional {
|
|
idx += 1;
|
|
format!(
|
|
"${{{}:{} ${{{}:{}}}}}",
|
|
idx - 1,
|
|
String::from_utf8_lossy(kwd),
|
|
idx,
|
|
text
|
|
)
|
|
} else {
|
|
format!("{} ${{{}:{}}}", String::from_utf8_lossy(kwd), idx, text)
|
|
}
|
|
}
|
|
_ => format!("${{{}:{}}}", idx, text),
|
|
}
|
|
};
|
|
|
|
for required in signature.required_positional {
|
|
snippet_text.push(' ');
|
|
snippet_text
|
|
.push_str(arg_wrapper(&required, required.name.clone(), false).as_str());
|
|
}
|
|
for optional in signature.optional_positional {
|
|
snippet_text.push(' ');
|
|
snippet_text
|
|
.push_str(arg_wrapper(&optional, format!("{}?", optional.name), true).as_str());
|
|
}
|
|
if let Some(rest) = signature.rest_positional {
|
|
idx += 1;
|
|
snippet_text.push_str(format!(" ${{{}:...{}}}", idx, rest.name).as_str());
|
|
}
|
|
}
|
|
// no extra space for a command with args expanded in the snippet
|
|
if idx == 0 && suggestion.suggestion.append_whitespace {
|
|
snippet_text.push(' ');
|
|
}
|
|
|
|
let text_edit = Some(CompletionTextEdit::Edit(TextEdit {
|
|
range,
|
|
new_text: snippet_text,
|
|
}));
|
|
|
|
CompletionItem {
|
|
label: suggestion.suggestion.value,
|
|
label_details: suggestion
|
|
.kind
|
|
.as_ref()
|
|
.map(|kind| match kind {
|
|
SuggestionKind::Value(t) => t.to_string(),
|
|
SuggestionKind::Command(cmd) => cmd.to_string(),
|
|
SuggestionKind::Module => "module".to_string(),
|
|
SuggestionKind::Operator => "operator".to_string(),
|
|
SuggestionKind::Variable => "variable".to_string(),
|
|
SuggestionKind::Flag => "flag".to_string(),
|
|
_ => String::new(),
|
|
})
|
|
.map(|s| CompletionItemLabelDetails {
|
|
detail: None,
|
|
description: Some(s),
|
|
}),
|
|
detail: suggestion.suggestion.description,
|
|
documentation: doc_string.map(|value| {
|
|
Documentation::MarkupContent(MarkupContent {
|
|
kind: MarkupKind::Markdown,
|
|
value,
|
|
})
|
|
}),
|
|
kind: Self::lsp_completion_item_kind(suggestion.kind),
|
|
text_edit,
|
|
insert_text_format,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn lsp_completion_item_kind(
|
|
suggestion_kind: Option<SuggestionKind>,
|
|
) -> Option<CompletionItemKind> {
|
|
suggestion_kind.and_then(|suggestion_kind| match suggestion_kind {
|
|
SuggestionKind::Value(t) => match t {
|
|
nu_protocol::Type::String => Some(CompletionItemKind::VALUE),
|
|
_ => None,
|
|
},
|
|
SuggestionKind::CellPath => Some(CompletionItemKind::PROPERTY),
|
|
SuggestionKind::Command(c) => match c {
|
|
CommandType::Keyword => Some(CompletionItemKind::KEYWORD),
|
|
CommandType::Builtin => Some(CompletionItemKind::FUNCTION),
|
|
CommandType::Alias => Some(CompletionItemKind::REFERENCE),
|
|
CommandType::External => Some(CompletionItemKind::INTERFACE),
|
|
CommandType::Custom | CommandType::Plugin => Some(CompletionItemKind::METHOD),
|
|
},
|
|
SuggestionKind::Directory => Some(CompletionItemKind::FOLDER),
|
|
SuggestionKind::File => Some(CompletionItemKind::FILE),
|
|
SuggestionKind::Flag => Some(CompletionItemKind::FIELD),
|
|
SuggestionKind::Module => Some(CompletionItemKind::MODULE),
|
|
SuggestionKind::Operator => Some(CompletionItemKind::OPERATOR),
|
|
SuggestionKind::Variable => Some(CompletionItemKind::VARIABLE),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::path_to_uri;
|
|
use crate::tests::{initialize_language_server, open_unchecked, result_from_message};
|
|
use assert_json_diff::assert_json_include;
|
|
use lsp_server::{Connection, Message};
|
|
use lsp_types::{
|
|
request::{Completion, Request},
|
|
CompletionParams, PartialResultParams, Position, TextDocumentIdentifier,
|
|
TextDocumentPositionParams, Uri, WorkDoneProgressParams,
|
|
};
|
|
use nu_test_support::fs::fixtures;
|
|
|
|
fn send_complete_request(
|
|
client_connection: &Connection,
|
|
uri: Uri,
|
|
line: u32,
|
|
character: u32,
|
|
) -> Message {
|
|
client_connection
|
|
.sender
|
|
.send(Message::Request(lsp_server::Request {
|
|
id: 2.into(),
|
|
method: Completion::METHOD.to_string(),
|
|
params: serde_json::to_value(CompletionParams {
|
|
text_document_position: TextDocumentPositionParams {
|
|
text_document: TextDocumentIdentifier { uri },
|
|
position: Position { line, character },
|
|
},
|
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
|
partial_result_params: PartialResultParams::default(),
|
|
context: None,
|
|
})
|
|
.unwrap(),
|
|
}))
|
|
.unwrap();
|
|
|
|
client_connection
|
|
.receiver
|
|
.recv_timeout(std::time::Duration::from_secs(2))
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn complete_on_variable() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("var.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
let resp = send_complete_request(&client_connection, script, 2, 9);
|
|
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "$greeting",
|
|
"labelDetails": { "description": "variable" },
|
|
"textEdit": {
|
|
"newText": "$greeting",
|
|
"range": { "start": { "character": 5, "line": 2 }, "end": { "character": 9, "line": 2 } }
|
|
},
|
|
"kind": 6
|
|
}
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_command() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("command.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
let resp = send_complete_request(&client_connection, script.clone(), 0, 6);
|
|
|
|
#[cfg(not(windows))]
|
|
let detail_str = "detail";
|
|
#[cfg(windows)]
|
|
let detail_str = "detail\r";
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
// defined after the cursor
|
|
{ "label": "config n foo bar", "detail": detail_str, "kind": 2 },
|
|
{
|
|
"label": "config nu",
|
|
"detail": "Edit nu configurations.",
|
|
"textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
|
|
"newText": "config nu "
|
|
},
|
|
},
|
|
])
|
|
);
|
|
|
|
// short flag
|
|
let resp = send_complete_request(&client_connection, script.clone(), 1, 18);
|
|
assert!(result_from_message(resp).as_array().unwrap().contains(
|
|
&serde_json::json!({
|
|
"label": "-s",
|
|
"detail": "test flag",
|
|
"labelDetails": { "description": "flag" },
|
|
"textEdit": { "range": { "start": { "line": 1, "character": 17 }, "end": { "line": 1, "character": 18 }, },
|
|
"newText": "-s "
|
|
},
|
|
"kind": 5
|
|
})
|
|
));
|
|
|
|
// long flag
|
|
let resp = send_complete_request(&client_connection, script.clone(), 2, 22);
|
|
assert!(result_from_message(resp).as_array().unwrap().contains(
|
|
&serde_json::json!({
|
|
"label": "--long",
|
|
"detail": "test flag",
|
|
"labelDetails": { "description": "flag" },
|
|
"textEdit": { "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 22 }, },
|
|
"newText": "--long "
|
|
},
|
|
"kind": 5
|
|
})
|
|
));
|
|
|
|
// file path
|
|
let resp = send_complete_request(&client_connection, script.clone(), 2, 18);
|
|
assert!(result_from_message(resp).as_array().unwrap().contains(
|
|
&serde_json::json!({
|
|
"label": "command.nu",
|
|
"labelDetails": { "description": "" },
|
|
"textEdit": { "range": { "start": { "line": 2, "character": 17 }, "end": { "line": 2, "character": 18 }, },
|
|
"newText": "command.nu"
|
|
},
|
|
"kind": 17
|
|
})
|
|
));
|
|
|
|
// inside parenthesis
|
|
let resp = send_complete_request(&client_connection, script, 10, 34);
|
|
assert!(result_from_message(resp).as_array().unwrap().contains(
|
|
&serde_json::json!({
|
|
"label": "-g",
|
|
"detail": "count indexes and split using grapheme clusters (all visible chars have length 1)",
|
|
"labelDetails": { "description": "flag" },
|
|
"textEdit": { "range": { "start": { "line": 10, "character": 33 }, "end": { "line": 10, "character": 34 }, },
|
|
"newText": "-g "
|
|
},
|
|
"kind": 5
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_completion() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("fallback.nu");
|
|
let script = path_to_uri(&script);
|
|
open_unchecked(&client_connection, script.clone());
|
|
|
|
// at the very beginning of a file
|
|
let resp = send_complete_request(&client_connection, script.clone(), 0, 0);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "alias",
|
|
"labelDetails": { "description": "keyword" },
|
|
"detail": "Alias a command (with optional flags) to a new name.",
|
|
"textEdit": {
|
|
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, },
|
|
"newText": "alias ${1:name} = ${2:initial_value}"
|
|
},
|
|
"insertTextFormat": 2,
|
|
"kind": 14
|
|
}
|
|
])
|
|
);
|
|
// after a white space character
|
|
let resp = send_complete_request(&client_connection, script.clone(), 3, 2);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "alias",
|
|
"labelDetails": { "description": "keyword" },
|
|
"detail": "Alias a command (with optional flags) to a new name.",
|
|
"textEdit": {
|
|
"range": { "start": { "line": 3, "character": 2 }, "end": { "line": 3, "character": 2 }, },
|
|
"newText": "alias ${1:name} = ${2:initial_value}"
|
|
},
|
|
"insertTextFormat": 2,
|
|
"kind": 14
|
|
}
|
|
])
|
|
);
|
|
// fallback file path completion
|
|
let resp = send_complete_request(&client_connection, script, 5, 4);
|
|
assert!(result_from_message(resp).as_array().unwrap().contains(
|
|
&serde_json::json!({
|
|
"label": "cell_path.nu",
|
|
"labelDetails": { "description": "" },
|
|
"textEdit": { "range": { "start": { "line": 5, "character": 3 }, "end": { "line": 5, "character": 4 }, },
|
|
"newText": "cell_path.nu"
|
|
},
|
|
"kind": 17
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn complete_command_with_line() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("utf_pipeline.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
let resp = send_complete_request(&client_connection, script, 0, 13);
|
|
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "str trim",
|
|
"labelDetails": { "description": "built-in" },
|
|
"detail": "Trim whitespace or specific character.",
|
|
"textEdit": {
|
|
"range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 }, },
|
|
"newText": "str trim ${1:...rest}"
|
|
},
|
|
"insertTextFormat": 2,
|
|
"kind": 3
|
|
}
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_keyword() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("keyword.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
let resp = send_complete_request(&client_connection, script, 0, 2);
|
|
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "overlay",
|
|
"labelDetails": { "description": "keyword" },
|
|
"textEdit": {
|
|
"newText": "overlay ",
|
|
"range": { "start": { "character": 0, "line": 0 }, "end": { "character": 2, "line": 0 } }
|
|
},
|
|
"kind": 14
|
|
},
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_cell_path() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("cell_path.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
let resp = send_complete_request(&client_connection, script.clone(), 1, 5);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "1",
|
|
"detail": "string",
|
|
"textEdit": {
|
|
"newText": "1",
|
|
"range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 5 } }
|
|
},
|
|
"kind": 10
|
|
},
|
|
])
|
|
);
|
|
|
|
let resp = send_complete_request(&client_connection, script, 1, 10);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "baz",
|
|
"detail": "int",
|
|
"textEdit": {
|
|
"newText": "baz",
|
|
"range": { "start": { "line": 1, "character": 10 }, "end": { "line": 1, "character": 10 } }
|
|
},
|
|
"kind": 10
|
|
},
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_with_external_completer() {
|
|
let config = "$env.config.completions.external.completer = {|spans| ['--background']}";
|
|
let (client_connection, _recv) = initialize_language_server(Some(config), None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("external.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
let resp = send_complete_request(&client_connection, script.clone(), 0, 11);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "--background",
|
|
"labelDetails": { "description": "string" },
|
|
"textEdit": {
|
|
"newText": "--background",
|
|
"range": { "start": { "line": 0, "character": 5 }, "end": { "line": 0, "character": 11 } }
|
|
},
|
|
},
|
|
])
|
|
);
|
|
|
|
// fallback completer, special argument treatment for `sudo`/`doas`
|
|
let resp = send_complete_request(&client_connection, script, 0, 5);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "alias",
|
|
"labelDetails": { "description": "keyword" },
|
|
"detail": "Alias a command (with optional flags) to a new name.",
|
|
"textEdit": {
|
|
"range": { "start": { "line": 0, "character": 5 }, "end": { "line": 0, "character": 5 }, },
|
|
"newText": "alias ${1:name} = ${2:initial_value}"
|
|
},
|
|
"kind": 14
|
|
},
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_operators() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("fallback.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
// fallback completer
|
|
let resp = send_complete_request(&client_connection, script.clone(), 7, 10);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "!=",
|
|
"labelDetails": { "description": "operator" },
|
|
"textEdit": {
|
|
"newText": "!= ",
|
|
"range": { "start": { "character": 10, "line": 7 }, "end": { "character": 10, "line": 7 } }
|
|
},
|
|
"kind": 24 // operator kind
|
|
}
|
|
])
|
|
);
|
|
|
|
let resp = send_complete_request(&client_connection, script.clone(), 7, 15);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "not-has",
|
|
"labelDetails": { "description": "operator" },
|
|
"textEdit": {
|
|
"newText": "not-has ",
|
|
"range": { "start": { "character": 10, "line": 7 }, "end": { "character": 15, "line": 7 } }
|
|
},
|
|
"kind": 24 // operator kind
|
|
}
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_use_arguments() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("completion");
|
|
script.push("use.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
let resp = send_complete_request(&client_connection, script.clone(), 4, 17);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "std-rfc",
|
|
"labelDetails": { "description": "module" },
|
|
"textEdit": {
|
|
"newText": "std-rfc",
|
|
"range": { "start": { "character": 11, "line": 4 }, "end": { "character": 17, "line": 4 } }
|
|
},
|
|
"kind": 9 // module kind
|
|
}
|
|
])
|
|
);
|
|
|
|
let resp = send_complete_request(&client_connection, script.clone(), 5, 22);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "clip",
|
|
"labelDetails": { "description": "module" },
|
|
"textEdit": {
|
|
"newText": "clip",
|
|
"range": { "start": { "character": 19, "line": 5 }, "end": { "character": 23, "line": 5 } }
|
|
},
|
|
"kind": 9 // module kind
|
|
}
|
|
])
|
|
);
|
|
|
|
let resp = send_complete_request(&client_connection, script.clone(), 5, 35);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "paste",
|
|
"labelDetails": { "description": "custom" },
|
|
"textEdit": {
|
|
"newText": "paste",
|
|
"range": { "start": { "character": 32, "line": 5 }, "end": { "character": 37, "line": 5 } }
|
|
},
|
|
"kind": 2
|
|
}
|
|
])
|
|
);
|
|
|
|
let resp = send_complete_request(&client_connection, script.clone(), 6, 14);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "null_device",
|
|
"labelDetails": { "description": "variable" },
|
|
"textEdit": {
|
|
"newText": "null_device",
|
|
"range": { "start": { "character": 8, "line": 6 }, "end": { "character": 14, "line": 6 } }
|
|
},
|
|
"kind": 6 // variable kind
|
|
}
|
|
])
|
|
);
|
|
|
|
let resp = send_complete_request(&client_connection, script, 7, 13);
|
|
assert_json_include!(
|
|
actual: result_from_message(resp),
|
|
expected: serde_json::json!([
|
|
{
|
|
"label": "foo",
|
|
"labelDetails": { "description": "variable" },
|
|
"textEdit": {
|
|
"newText": "foo",
|
|
"range": { "start": { "character": 11, "line": 7 }, "end": { "character": 14, "line": 7 } }
|
|
},
|
|
"kind": 6 // variable kind
|
|
}
|
|
])
|
|
);
|
|
}
|
|
}
|