#![doc = include_str!("../README.md")] use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; use lsp_types::{ request::{Completion, GotoDefinition, HoverRequest, Request}, CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind, OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit, Url, }; use miette::{IntoDiagnostic, Result}; use nu_cli::{NuCompleter, SuggestionKind}; use nu_parser::{flatten_block, parse, FlatShape}; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, DeclId, Span, Value, VarId, }; use ropey::Rope; use std::{ collections::BTreeMap, path::{Path, PathBuf}, sync::Arc, time::Duration, }; mod diagnostics; mod notification; #[derive(Debug)] enum Id { Variable(VarId), Declaration(DeclId), Value(FlatShape), } pub struct LanguageServer { connection: Connection, io_threads: Option, ropes: BTreeMap, } impl LanguageServer { pub fn initialize_stdio_connection() -> Result { let (connection, io_threads) = Connection::stdio(); Self::initialize_connection(connection, Some(io_threads)) } fn initialize_connection( connection: Connection, io_threads: Option, ) -> Result { Ok(Self { connection, io_threads, ropes: BTreeMap::new(), }) } pub fn serve_requests(mut self, engine_state: EngineState) -> Result<()> { let server_capabilities = serde_json::to_value(ServerCapabilities { text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind( TextDocumentSyncKind::INCREMENTAL, )), 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_while(server_capabilities, || { !engine_state.signals().interrupted() }) .into_diagnostic()?; while !engine_state.signals().interrupted() { let msg = match self .connection .receiver .recv_timeout(Duration::from_secs(1)) { Ok(msg) => msg, Err(crossbeam_channel::RecvTimeoutError::Timeout) => { continue; } Err(_) => break, }; match msg { Message::Request(request) => { if self .connection .handle_shutdown(&request) .into_diagnostic()? { return Ok(()); } let mut engine_state = engine_state.clone(); let resp = match request.method.as_str() { GotoDefinition::METHOD => Self::handle_lsp_request( &mut engine_state, request, |engine_state, params| self.goto_definition(engine_state, params), ), HoverRequest::METHOD => Self::handle_lsp_request( &mut engine_state, request, |engine_state, params| self.hover(engine_state, params), ), Completion::METHOD => Self::handle_lsp_request( &mut engine_state, request, |engine_state, params| self.complete(engine_state, params), ), _ => { continue; } }; self.connection .sender .send(Message::Response(resp)) .into_diagnostic()?; } Message::Response(_) => {} Message::Notification(notification) => { if let Some(updated_file) = self.handle_lsp_notification(notification) { let mut engine_state = engine_state.clone(); self.publish_diagnostics_for_file(updated_file, &mut engine_state)?; } } } } if let Some(io_threads) = self.io_threads { io_threads.join().into_diagnostic()?; } Ok(()) } fn handle_lsp_request( engine_state: &mut EngineState, req: lsp_server::Request, mut param_handler: H, ) -> Response where P: serde::de::DeserializeOwned, H: FnMut(&mut EngineState, &P) -> Option, R: serde::ser::Serialize, { match serde_json::from_value::

(req.params) { Ok(params) => Response { id: req.id, result: Some( param_handler(engine_state, ¶ms) .and_then(|response| serde_json::to_value(response).ok()) .unwrap_or(serde_json::Value::Null), ), error: None, }, Err(err) => Response { id: req.id, result: None, error: Some(ResponseError { code: 1, message: err.to_string(), data: None, }), }, } } fn span_to_range(span: &Span, rope_of_file: &Rope, offset: usize) -> lsp_types::Range { let line = rope_of_file.byte_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.byte_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 } } pub 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, path: &Path, file: &Rope, location: usize, ) -> Option<(Id, usize, Span)> { let file_path = path.to_string_lossy(); // TODO: think about passing down the rope into the working_set let contents = file.bytes().collect::>(); let block = parse(working_set, Some(&file_path), &contents, false); let flattened = flatten_block(working_set, &block); let offset = working_set.get_span_for_filename(&file_path)?.start; let location = location + offset; for (span, shape) in flattened { if location >= span.start && location < span.end { match &shape { FlatShape::Variable(var_id) | FlatShape::VarDecl(var_id) => { return Some((Id::Variable(*var_id), offset, span)); } FlatShape::InternalCall(decl_id) => { return Some((Id::Declaration(*decl_id), offset, span)); } _ => return Some((Id::Value(shape), offset, span)), } } } None } fn rope<'a, 'b: 'a>(&'b self, file_url: &Url) -> Option<(&'a Rope, &'a PathBuf)> { let file_path = file_url.to_file_path().ok()?; self.ropes .get_key_value(&file_path) .map(|(path, rope)| (rope, path)) } fn read_in_file<'a>( &mut self, engine_state: &'a mut EngineState, file_url: &Url, ) -> Option<(&Rope, &PathBuf, StateWorkingSet<'a>)> { let (file, path) = self.rope(file_url)?; engine_state.file = Some(path.to_owned()); let working_set = StateWorkingSet::new(engine_state); Some((file, path, working_set)) } fn goto_definition( &mut self, engine_state: &mut EngineState, params: &GotoDefinitionParams, ) -> Option { 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, mut working_set) = self.read_in_file( engine_state, ¶ms.text_document_position_params.text_document.uri, )?; let (id, _, _) = Self::find_id( &mut working_set, path, file, Self::lsp_position_to_location(¶ms.text_document_position_params.position, file), )?; match id { Id::Declaration(decl_id) => { if let Some(block_id) = working_set.get_decl(decl_id).block_id() { let block = working_set.get_block(block_id); if let Some(span) = &block.span { for cached_file in working_set.files() { if cached_file.covered_span.contains(span.start) { return Some(GotoDefinitionResponse::Scalar(Location { uri: Url::from_file_path(&*cached_file.name).ok()?, range: Self::span_to_range( span, file, cached_file.covered_span.start, ), })); } } } } } Id::Variable(var_id) => { let var = working_set.get_variable(var_id); for cached_file in working_set.files() { if cached_file .covered_span .contains(var.declaration_span.start) { return Some(GotoDefinitionResponse::Scalar(Location { uri: params .text_document_position_params .text_document .uri .clone(), range: Self::span_to_range( &var.declaration_span, file, cached_file.covered_span.start, ), })); } } } Id::Value(_) => {} } None } fn hover(&mut self, engine_state: &mut EngineState, params: &HoverParams) -> Option { 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, mut working_set) = self.read_in_file( engine_state, ¶ms.text_document_position_params.text_document.uri, )?; let (id, _, _) = Self::find_id( &mut working_set, path, file, Self::lsp_position_to_location(¶ms.text_document_position_params.position, 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 = String::new(); // First description description.push_str(&format!("{}\n", decl.description().replace('\r', ""))); // Additional description if !decl.extra_description().is_empty() { description.push_str(&format!("\n{}\n", decl.extra_description())); } // Usage description.push_str("### Usage \n```nu\n"); 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"); // Flags if !signature.named.is_empty() { description.push_str("\n### Flags\n\n"); let mut first = true; for named in &signature.named { if first { first = false; } else { description.push('\n'); } 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'); } description.push('\n'); } // Parameters 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 { first = false; } else { description.push('\n'); } 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 { first = false; } else { description.push('\n'); } 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('\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'); } // Input/output types if !signature.input_output_types.is_empty() { description.push_str("\n### Input/output types\n"); description.push_str("\n```nu\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"); } // Examples if !decl.examples().is_empty() { description.push_str("### Example(s)\n"); for example in decl.examples() { description.push_str(&format!( " {}\n```nu\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( &mut self, engine_state: &mut EngineState, params: &CompletionParams, ) -> Option { 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 (rope_of_file, _, _) = self.read_in_file( engine_state, ¶ms.text_document_position.text_document.uri, )?; let mut completer = NuCompleter::new(Arc::new(engine_state.clone()), Arc::new(Stack::new())); let location = Self::lsp_position_to_location(¶ms.text_document_position.position, rope_of_file); let results = completer.fetch_completions_at(&rope_of_file.to_string()[..location], 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.suggestion.span.end - r.suggestion.span.start) as u32; CompletionItem { label: r.suggestion.value.clone(), detail: r.suggestion.description, kind: Self::lsp_completion_item_kind(r.kind), text_edit: Some(CompletionTextEdit::Edit(TextEdit { range: Range { start, end: params.text_document_position.position, }, new_text: r.suggestion.value, })), ..Default::default() } }) .collect(), )) } } fn lsp_completion_item_kind( suggestion_kind: Option, ) -> Option { suggestion_kind.and_then(|suggestion_kind| match suggestion_kind { SuggestionKind::Type(t) => match t { nu_protocol::Type::String => Some(CompletionItemKind::VARIABLE), _ => None, }, SuggestionKind::Command(c) => match c { nu_protocol::engine::CommandType::Keyword => Some(CompletionItemKind::KEYWORD), nu_protocol::engine::CommandType::Builtin => Some(CompletionItemKind::FUNCTION), _ => None, }, }) } } #[cfg(test)] mod tests { use super::*; use assert_json_diff::{assert_json_eq, assert_json_include}; use lsp_types::{ notification::{ DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification, }, request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown}, CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, GotoDefinitionParams, InitializeParams, InitializedParams, PartialResultParams, TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, Url, WorkDoneProgressParams, }; use nu_test_support::fs::{fixtures, root}; use std::sync::mpsc::Receiver; pub fn initialize_language_server() -> (Connection, Receiver>) { 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: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), }) .unwrap(), })) .unwrap(); let resp = client_connection .receiver .recv_timeout(std::time::Duration::from_secs(2)) .unwrap(); let result = if let Message::Response(response) = resp { response.result } else { panic!() }; assert_json_eq!(result, serde_json::json!(null)); } pub fn open_unchecked(client_connection: &Connection, uri: Url) -> lsp_server::Notification { open(client_connection, uri).unwrap() } pub fn open( client_connection: &Connection, uri: Url, ) -> Result { let text = std::fs::read_to_string(uri.to_file_path().unwrap()).map_err(|e| e.to_string())?; client_connection .sender .send(Message::Notification(lsp_server::Notification { method: DidOpenTextDocument::METHOD.to_string(), params: serde_json::to_value(DidOpenTextDocumentParams { text_document: TextDocumentItem { uri, language_id: String::from("nu"), version: 1, text, }, }) .unwrap(), })) .map_err(|e| e.to_string())?; let notification = client_connection .receiver .recv_timeout(Duration::from_secs(2)) .map_err(|e| e.to_string())?; if let Message::Notification(n) = notification { Ok(n) } else { Err(String::from("Did not receive a notification from server")) } } pub fn update( client_connection: &Connection, uri: Url, text: String, range: Option, ) -> lsp_server::Notification { client_connection .sender .send(lsp_server::Message::Notification( lsp_server::Notification { method: DidChangeTextDocument::METHOD.to_string(), params: serde_json::to_value(DidChangeTextDocumentParams { text_document: lsp_types::VersionedTextDocumentIdentifier { uri, version: 2, }, content_changes: vec![TextDocumentContentChangeEvent { range, range_length: None, text, }], }) .unwrap(), }, )) .unwrap(); let notification = client_connection .receiver .recv_timeout(Duration::from_secs(2)) .unwrap(); if let Message::Notification(n) = notification { n } else { panic!(); } } fn goto_definition( client_connection: &Connection, uri: Url, line: u32, character: u32, ) -> Message { 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: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), }) .unwrap(), })) .unwrap(); client_connection .receiver .recv_timeout(std::time::Duration::from_secs(2)) .unwrap() } #[test] fn goto_definition_of_variable() { let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("goto"); script.push("var.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = goto_definition(&client_connection, 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 (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("goto"); script.push("command.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = goto_definition(&client_connection, 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 (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("goto"); script.push("command.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = goto_definition(&client_connection, 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 } } }) ); } pub fn hover(client_connection: &Connection, uri: Url, line: u32, character: u32) -> Message { 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: WorkDoneProgressParams::default(), }) .unwrap(), })) .unwrap(); client_connection .receiver .recv_timeout(std::time::Duration::from_secs(2)) .unwrap() } #[test] fn hover_on_variable() { let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("var.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = hover(&client_connection, 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_custom_command() { let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("command.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = hover(&client_connection, 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": "Renders some greeting message\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n" } }) ); } #[test] fn hover_on_str_join() { let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("command.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = hover(&client_connection, script.clone(), 5, 8); let result = if let Message::Response(response) = resp { response.result } else { panic!() }; assert_json_eq!( result, serde_json::json!({ "contents": { "kind": "markdown", "value": "Concatenate multiple strings into a single string, with an optional separator between each.\n### Usage \n```nu\n str join {flags} \n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `separator: string` - Optional separator to use when creating string.\n\n\n### Input/output types\n\n```nu\n list | string\n string | string\n\n```\n### Example(s)\n Create a string from input\n```nu\n ['nu', 'shell'] | str join\n```\n Create a string from input with a separator\n```nu\n ['nu', 'shell'] | str join '-'\n```\n" } }) ); } fn complete(client_connection: &Connection, uri: Url, 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: lsp_types::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(); let mut script = fixtures(); script.push("lsp"); script.push("completion"); script.push("var.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = complete(&client_connection, 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 } } }, "kind": 6 } ]) ); } #[test] fn complete_command_with_space() { let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("completion"); script.push("command.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = complete(&client_connection, 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" }, "kind": 3 } ]) ); } #[test] fn complete_command_with_utf_line() { let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("completion"); script.push("utf_pipeline.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = complete(&client_connection, script, 0, 14); let result = if let Message::Response(response) = resp { response.result } else { panic!() }; assert_json_eq!( result, serde_json::json!([ { "label": "str trim", "detail": "Trim whitespace or specific character.", "textEdit": { "range": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 14 }, }, "newText": "str trim" }, "kind": 3 } ]) ); } #[test] fn complete_keyword() { let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("completion"); script.push("keyword.nu"); let script = Url::from_file_path(script).unwrap(); open_unchecked(&client_connection, script.clone()); let resp = complete(&client_connection, script, 0, 2); let result = if let Message::Response(response) = resp { response.result } else { panic!() }; assert_json_include!( actual: result, expected: serde_json::json!([ { "label": "overlay", "textEdit": { "newText": "overlay", "range": { "start": { "character": 0, "line": 0 }, "end": { "character": 2, "line": 0 } } }, "kind": 14 }, ]) ); } }