nushell/crates/nu-lsp/src/lib.rs
Marc Schreiber 0ca8fcf58c
Integrated Language Server (#10723)
# Description

This commit integrates a language server into nushell so that IDEs don't
have to convert CLI option back and forth.

- fixes https://github.com/nushell/vscode-nushell-lang/issues/117
- fixes https://github.com/jokeyrhyme/nuls/issues/8

Tracking tasks


- [x]
[textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover)
-> `nu --ide-hover`
- [x]
[textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion)
-> `nu --ide-complete`
- [x]
[textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition)
-> `nu --ide-goto-def`
- ~~[ ]
[textDocument/didChange](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange),
[textDocument/didClose](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didClose),
and
[textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen)~~
(will be done in a follow-up PR)
- ~~[ ]
[textDocument/inlayHint](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint)
-> `nu --ide-check`~~ (will be done in a follow-up PR)
- ~~[ ]
[textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics)
-> `nu --ide-check`~~ (will be done in a follow-up PR)
- ~~[ ]
[workspace/configuration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration)~~
(will be done in a follow-up PR)
- ~~[ ]
[workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration)~~
(will be done in a follow-up PR)


# User-Facing Changes

The command line options `--lsp` will start a LSP server.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use std testing; testing run-tests --path
crates/nu-std"` to run the tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2023-11-02 10:18:57 -05:00

934 lines
33 KiB
Rust

use std::{fs::File, io::Cursor, sync::Arc};
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
use lsp_types::{
request::{Completion, GotoDefinition, HoverRequest, Request},
CompletionItem, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams,
GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind,
OneOf, Range, ServerCapabilities, TextEdit, Url,
};
use miette::{IntoDiagnostic, Result};
use nu_cli::NuCompleter;
use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
DeclId, Span, Value, VarId,
};
use reedline::Completer;
use ropey::Rope;
#[derive(Debug)]
enum Id {
Variable(VarId),
Declaration(DeclId),
Value(FlatShape),
}
pub struct LanguageServer {
connection: Connection,
io_threads: Option<IoThreads>,
}
impl LanguageServer {
pub fn initialize_stdio_connection() -> Result<Self> {
let (connection, io_threads) = Connection::stdio();
Self::initialize_connection(connection, Some(io_threads))
}
fn initialize_connection(
connection: Connection,
io_threads: Option<IoThreads>,
) -> Result<Self> {
Ok(Self {
connection,
io_threads,
})
}
pub fn serve_requests(self, engine_state: EngineState) -> Result<()> {
let server_capabilities = serde_json::to_value(&ServerCapabilities {
definition_provider: Some(OneOf::Left(true)),
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
completion_provider: Some(lsp_types::CompletionOptions::default()),
..Default::default()
})
.expect("Must be serializable");
let _initialization_params = self
.connection
.initialize(server_capabilities)
.into_diagnostic()?;
for msg in &self.connection.receiver {
match msg {
Message::Request(request) => {
if self
.connection
.handle_shutdown(&request)
.into_diagnostic()?
{
return Ok(());
}
let mut engine_state = engine_state.clone();
match request.method.as_str() {
GotoDefinition::METHOD => {
self.handle_lsp_request(
&mut engine_state,
request,
Self::goto_definition,
)?;
}
HoverRequest::METHOD => {
self.handle_lsp_request(&mut engine_state, request, Self::hover)?;
}
Completion::METHOD => {
self.handle_lsp_request(&mut engine_state, request, Self::complete)?;
}
_ => {}
}
}
Message::Response(_) => {}
Message::Notification(_) => {}
}
}
if let Some(io_threads) = self.io_threads {
io_threads.join().into_diagnostic()?;
}
Ok(())
}
fn handle_lsp_request<P, H, R>(
&self,
engine_state: &mut EngineState,
req: lsp_server::Request,
param_handler: H,
) -> Result<()>
where
P: serde::de::DeserializeOwned,
H: Fn(&mut EngineState, &P) -> Option<R>,
R: serde::ser::Serialize,
{
let resp = {
match serde_json::from_value::<P>(req.params) {
Ok(params) => Response {
id: req.id,
result: param_handler(engine_state, &params)
.and_then(|response| serde_json::to_value(response).ok()),
error: None,
},
Err(err) => Response {
id: req.id,
result: None,
error: Some(ResponseError {
code: 1,
message: err.to_string(),
data: None,
}),
},
}
};
self.connection
.sender
.send(Message::Response(resp))
.into_diagnostic()
}
fn span_to_range(span: &Span, rope_of_file: &Rope, offset: usize) -> lsp_types::Range {
let line = rope_of_file.char_to_line(span.start - offset);
let character = span.start - offset - rope_of_file.line_to_char(line);
let start = lsp_types::Position {
line: line as u32,
character: character as u32,
};
let line = rope_of_file.char_to_line(span.end - offset);
let character = span.end - offset - rope_of_file.line_to_char(line);
let end = lsp_types::Position {
line: line as u32,
character: character as u32,
};
lsp_types::Range { start, end }
}
fn lsp_position_to_location(position: &lsp_types::Position, rope_of_file: &Rope) -> usize {
let line_idx = rope_of_file.line_to_char(position.line as usize);
line_idx + position.character as usize
}
fn find_id(
working_set: &mut StateWorkingSet,
file_path: &str,
file: &[u8],
location: usize,
) -> Option<(Id, usize, Span)> {
let file_id = working_set.add_file(file_path.to_string(), file);
let offset = working_set.get_span_for_file(file_id).start;
let block = parse(working_set, Some(file_path), file, false);
let flattened = flatten_block(working_set, &block);
let location = location + offset;
for item in flattened {
if location >= item.0.start && location < item.0.end {
match &item.1 {
FlatShape::Variable(var_id) | FlatShape::VarDecl(var_id) => {
return Some((Id::Variable(*var_id), offset, item.0));
}
FlatShape::InternalCall(decl_id) => {
return Some((Id::Declaration(*decl_id), offset, item.0));
}
_ => return Some((Id::Value(item.1), offset, item.0)),
}
}
}
None
}
fn read_in_file<'a>(
engine_state: &'a mut EngineState,
file_path: &str,
) -> Result<(Vec<u8>, StateWorkingSet<'a>)> {
let file = std::fs::read(file_path).into_diagnostic()?;
engine_state.start_in_file(Some(file_path));
let working_set = StateWorkingSet::new(engine_state);
Ok((file, working_set))
}
fn goto_definition(
engine_state: &mut EngineState,
params: &GotoDefinitionParams,
) -> Option<GotoDefinitionResponse> {
let cwd = std::env::current_dir().expect("Could not get current working directory.");
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
let file_path = params
.text_document_position_params
.text_document
.uri
.to_file_path()
.ok()?;
let file_path = file_path.to_string_lossy();
let (file, mut working_set) = Self::read_in_file(engine_state, &file_path).ok()?;
let rope_of_file = Rope::from_reader(Cursor::new(&file)).ok()?;
let (id, _, _) = Self::find_id(
&mut working_set,
&file_path,
&file,
Self::lsp_position_to_location(
&params.text_document_position_params.position,
&rope_of_file,
),
)?;
match id {
Id::Declaration(decl_id) => {
if let Some(block_id) = working_set.get_decl(decl_id).get_block_id() {
let block = working_set.get_block(block_id);
if let Some(span) = &block.span {
for (file_path, file_start, file_end) in working_set.files() {
if span.start >= *file_start && span.start < *file_end {
return Some(GotoDefinitionResponse::Scalar(Location {
uri: Url::from_file_path(file_path).ok()?,
range: Self::span_to_range(span, &rope_of_file, *file_start),
}));
}
}
}
}
}
Id::Variable(var_id) => {
let var = working_set.get_variable(var_id);
for (_, file_start, file_end) in working_set.files() {
if var.declaration_span.start >= *file_start
&& var.declaration_span.start < *file_end
{
return Some(GotoDefinitionResponse::Scalar(Location {
uri: params
.text_document_position_params
.text_document
.uri
.clone(),
range: Self::span_to_range(
&var.declaration_span,
&rope_of_file,
*file_start,
),
}));
}
}
}
_ => {}
}
None
}
fn hover(engine_state: &mut EngineState, params: &HoverParams) -> Option<Hover> {
let cwd = std::env::current_dir().expect("Could not get current working directory.");
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
let file_path = params
.text_document_position_params
.text_document
.uri
.to_file_path()
.ok()?;
let file_path = file_path.to_string_lossy();
let (file, mut working_set) = Self::read_in_file(engine_state, &file_path).ok()?;
let rope_of_file = Rope::from_reader(Cursor::new(&file)).ok()?;
let (id, _, _) = Self::find_id(
&mut working_set,
&file_path,
&file,
Self::lsp_position_to_location(
&params.text_document_position_params.position,
&rope_of_file,
),
)?;
match id {
Id::Variable(var_id) => {
let var = working_set.get_variable(var_id);
let contents = format!("{}{}", if var.mutable { "mutable " } else { "" }, var.ty);
Some(Hover {
contents: HoverContents::Scalar(lsp_types::MarkedString::String(contents)),
// TODO
range: None,
})
}
Id::Declaration(decl_id) => {
let decl = working_set.get_decl(decl_id);
let mut description = "```\n### Signature\n```\n".to_string();
let signature = decl.signature();
description.push_str(&format!(" {}", signature.name));
if !signature.named.is_empty() {
description.push_str(" {flags}")
}
for required_arg in &signature.required_positional {
description.push_str(&format!(" <{}>", required_arg.name));
}
for optional_arg in &signature.optional_positional {
description.push_str(&format!(" <{}?>", optional_arg.name));
}
if let Some(arg) = &signature.rest_positional {
description.push_str(&format!(" <...{}>", arg.name));
}
description.push_str("\n```\n");
if !signature.required_positional.is_empty()
|| !signature.optional_positional.is_empty()
|| signature.rest_positional.is_some()
{
description.push_str("\n### Parameters\n\n");
let mut first = true;
for required_arg in &signature.required_positional {
if !first {
description.push_str("\\\n");
} else {
first = false;
}
description.push_str(&format!(
" `{}: {}`",
required_arg.name,
required_arg.shape.to_type()
));
if !required_arg.desc.is_empty() {
description.push_str(&format!(" - {}", required_arg.desc));
}
description.push('\n');
}
for optional_arg in &signature.optional_positional {
if !first {
description.push_str("\\\n");
} else {
first = false;
}
description.push_str(&format!(
" `{}: {}`",
optional_arg.name,
optional_arg.shape.to_type()
));
if !optional_arg.desc.is_empty() {
description.push_str(&format!(" - {}", optional_arg.desc));
}
description.push('\n');
}
if let Some(arg) = &signature.rest_positional {
if !first {
description.push_str("\\\n");
}
description.push_str(&format!(
" `...{}: {}`",
arg.name,
arg.shape.to_type()
));
if !arg.desc.is_empty() {
description.push_str(&format!(" - {}", arg.desc));
}
description.push('\n');
}
description.push('\n');
}
if !signature.named.is_empty() {
description.push_str("\n### Flags\n\n");
let mut first = true;
for named in &signature.named {
if !first {
description.push_str("\\\n");
} else {
first = false;
}
description.push_str(" ");
if let Some(short_flag) = &named.short {
description.push_str(&format!("`-{}`", short_flag));
}
if !named.long.is_empty() {
if named.short.is_some() {
description.push_str(", ")
}
description.push_str(&format!("`--{}`", named.long));
}
if let Some(arg) = &named.arg {
description.push_str(&format!(" `<{}>`", arg.to_type()))
}
if !named.desc.is_empty() {
description.push_str(&format!(" - {}", named.desc));
}
}
description.push('\n');
}
if !signature.input_output_types.is_empty() {
description.push_str("\n### Input/output\n");
description.push_str("\n```\n");
for input_output in &signature.input_output_types {
description
.push_str(&format!(" {} | {}\n", input_output.0, input_output.1));
}
description.push_str("\n```\n");
}
description.push_str(&format!(
"### Usage\n {}\n",
decl.usage().replace('\r', "")
));
if !decl.extra_usage().is_empty() {
description
.push_str(&format!("\n### Extra usage:\n {}\n", decl.extra_usage()));
}
if !decl.examples().is_empty() {
description.push_str("### Example(s)\n```\n");
for example in decl.examples() {
description.push_str(&format!(
"```\n {}\n```\n {}\n\n",
example.description, example.example
));
}
}
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: description,
}),
// TODO
range: None,
})
}
Id::Value(shape) => {
let hover = String::from(match shape {
FlatShape::And => "and",
FlatShape::Binary => "binary",
FlatShape::Block => "block",
FlatShape::Bool => "bool",
FlatShape::Closure => "closure",
FlatShape::DateTime => "datetime",
FlatShape::Directory => "directory",
FlatShape::External => "external",
FlatShape::ExternalArg => "external arg",
FlatShape::Filepath => "file path",
FlatShape::Flag => "flag",
FlatShape::Float => "float",
FlatShape::GlobPattern => "glob pattern",
FlatShape::Int => "int",
FlatShape::Keyword => "keyword",
FlatShape::List => "list",
FlatShape::MatchPattern => "match-pattern",
FlatShape::Nothing => "nothing",
FlatShape::Range => "range",
FlatShape::Record => "record",
FlatShape::String => "string",
FlatShape::StringInterpolation => "string interpolation",
FlatShape::Table => "table",
_ => {
return None;
}
});
Some(Hover {
contents: HoverContents::Scalar(lsp_types::MarkedString::String(hover)),
// TODO
range: None,
})
}
}
}
fn complete(
engine_state: &mut EngineState,
params: &CompletionParams,
) -> Option<CompletionResponse> {
let cwd = std::env::current_dir().expect("Could not get current working directory.");
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
let file_path = params
.text_document_position
.text_document
.uri
.to_file_path()
.ok()?;
let file_path = file_path.to_string_lossy();
let rope_of_file = Rope::from_reader(File::open(file_path.as_ref()).ok()?).ok()?;
let stack = Stack::new();
let mut completer = NuCompleter::new(Arc::new(engine_state.clone()), stack);
let location =
Self::lsp_position_to_location(&params.text_document_position.position, &rope_of_file);
let results = completer.complete(&rope_of_file.to_string(), location);
if results.is_empty() {
None
} else {
Some(CompletionResponse::Array(
results
.into_iter()
.map(|r| {
let mut start = params.text_document_position.position;
start.character -= (r.span.end - r.span.start) as u32;
CompletionItem {
label: r.value.clone(),
detail: r.description,
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
range: Range {
start,
end: params.text_document_position.position,
},
new_text: r.value,
})),
..Default::default()
}
})
.collect(),
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_json_diff::assert_json_eq;
use lsp_types::{
notification::{Exit, Initialized, Notification},
request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown},
CompletionParams, GotoDefinitionParams, InitializeParams, InitializedParams,
TextDocumentIdentifier, TextDocumentPositionParams, Url,
};
use nu_test_support::fs::{fixtures, root};
use std::sync::mpsc::Receiver;
fn initialize_language_server() -> (Connection, Receiver<Result<()>>) {
use std::sync::mpsc;
let (client_connection, server_connection) = Connection::memory();
let lsp_server = LanguageServer::initialize_connection(server_connection, None).unwrap();
let (send, recv) = mpsc::channel();
std::thread::spawn(move || {
let engine_state = nu_cmd_lang::create_default_context();
let engine_state = nu_command::add_shell_command_context(engine_state);
send.send(lsp_server.serve_requests(engine_state))
});
client_connection
.sender
.send(Message::Request(lsp_server::Request {
id: 1.into(),
method: Initialize::METHOD.to_string(),
params: serde_json::to_value(InitializeParams {
..Default::default()
})
.unwrap(),
}))
.unwrap();
client_connection
.sender
.send(Message::Notification(lsp_server::Notification {
method: Initialized::METHOD.to_string(),
params: serde_json::to_value(InitializedParams {}).unwrap(),
}))
.unwrap();
let _initialize_response = client_connection
.receiver
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
(client_connection, recv)
}
#[test]
fn shutdown_on_request() {
let (client_connection, recv) = initialize_language_server();
client_connection
.sender
.send(Message::Request(lsp_server::Request {
id: 2.into(),
method: Shutdown::METHOD.to_string(),
params: serde_json::Value::Null,
}))
.unwrap();
client_connection
.sender
.send(Message::Notification(lsp_server::Notification {
method: Exit::METHOD.to_string(),
params: serde_json::Value::Null,
}))
.unwrap();
assert!(recv
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap()
.is_ok());
}
#[test]
fn goto_definition_for_none_existing_file() {
let (client_connection, _recv) = initialize_language_server();
let mut none_existent_path = root();
none_existent_path.push("none-existent.nu");
client_connection
.sender
.send(Message::Request(lsp_server::Request {
id: 2.into(),
method: GotoDefinition::METHOD.to_string(),
params: serde_json::to_value(GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: Url::from_file_path(none_existent_path).unwrap(),
},
position: lsp_types::Position {
line: 0,
character: 0,
},
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.unwrap(),
}))
.unwrap();
let resp = client_connection
.receiver
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
assert!(matches!(
resp,
Message::Response(response) if response.result.is_none()
));
}
fn goto_definition(uri: Url, line: u32, character: u32) -> Message {
let (client_connection, _recv) = initialize_language_server();
client_connection
.sender
.send(Message::Request(lsp_server::Request {
id: 2.into(),
method: GotoDefinition::METHOD.to_string(),
params: serde_json::to_value(GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: lsp_types::Position { line, character },
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.unwrap(),
}))
.unwrap();
client_connection
.receiver
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap()
}
#[test]
fn goto_definition_of_variable() {
let mut script = fixtures();
script.push("lsp");
script.push("goto");
script.push("var.nu");
let script = Url::from_file_path(script).unwrap();
let resp = goto_definition(script.clone(), 2, 12);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!({
"uri": script,
"range": {
"start": { "line": 0, "character": 4 },
"end": { "line": 0, "character": 12 }
}
})
);
}
#[test]
fn goto_definition_of_command() {
let mut script = fixtures();
script.push("lsp");
script.push("goto");
script.push("command.nu");
let script = Url::from_file_path(script).unwrap();
let resp = goto_definition(script.clone(), 4, 1);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!({
"uri": script,
"range": {
"start": { "line": 0, "character": 17 },
"end": { "line": 2, "character": 1 }
}
})
);
}
#[test]
fn goto_definition_of_command_parameter() {
let mut script = fixtures();
script.push("lsp");
script.push("goto");
script.push("command.nu");
let script = Url::from_file_path(script).unwrap();
let resp = goto_definition(script.clone(), 1, 14);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!({
"uri": script,
"range": {
"start": { "line": 0, "character": 11 },
"end": { "line": 0, "character": 15 }
}
})
);
}
fn hover(uri: Url, line: u32, character: u32) -> Message {
let (client_connection, _recv) = initialize_language_server();
client_connection
.sender
.send(Message::Request(lsp_server::Request {
id: 2.into(),
method: HoverRequest::METHOD.to_string(),
params: serde_json::to_value(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: lsp_types::Position { line, character },
},
work_done_progress_params: Default::default(),
})
.unwrap(),
}))
.unwrap();
client_connection
.receiver
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap()
}
#[test]
fn hover_on_variable() {
let mut script = fixtures();
script.push("lsp");
script.push("hover");
script.push("var.nu");
let script = Url::from_file_path(script).unwrap();
let resp = hover(script.clone(), 2, 0);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!({
"contents": "table"
})
);
}
#[test]
fn hover_on_command() {
let mut script = fixtures();
script.push("lsp");
script.push("hover");
script.push("command.nu");
let script = Url::from_file_path(script).unwrap();
let resp = hover(script.clone(), 3, 0);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!({
"contents": {
"kind": "markdown",
"value": "```\n### Signature\n```\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n### Usage\n Renders some greeting message\n"
}
})
);
}
fn complete(uri: Url, line: u32, character: u32) -> Message {
let (client_connection, _recv) = initialize_language_server();
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: lsp_types::Position { line, character },
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: None,
})
.unwrap(),
}))
.unwrap();
client_connection
.receiver
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap()
}
#[test]
fn complete_on_variable() {
let mut script = fixtures();
script.push("lsp");
script.push("completion");
script.push("var.nu");
let script = Url::from_file_path(script).unwrap();
let resp = complete(script, 2, 9);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!([
{
"label": "$greeting",
"textEdit": {
"newText": "$greeting",
"range": {
"start": { "character": 5, "line": 2 },
"end": { "character": 9, "line": 2 }
}
}
}
])
);
}
#[test]
fn complete_command_with_space() {
let mut script = fixtures();
script.push("lsp");
script.push("completion");
script.push("command.nu");
let script = Url::from_file_path(script).unwrap();
let resp = complete(script, 0, 8);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!([
{
"label": "config nu",
"detail": "Edit nu configurations.",
"textEdit": {
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 8 },
},
"newText": "config nu"
}
}
])
);
}
}