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, } 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, }) } 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( &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: serde::ser::Serialize, { let resp = { match serde_json::from_value::

(req.params) { Ok(params) => Response { id: req.id, result: param_handler(engine_state, ¶ms) .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, 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 { 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( ¶ms.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 { 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( ¶ms.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 { 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(¶ms.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>) { 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" } } ]) ); } }