From c110ddff6625a4c98176a1d91ead024b83a0f0d8 Mon Sep 17 00:00:00 2001 From: Marc Schreiber Date: Thu, 16 Nov 2023 00:35:48 +0100 Subject: [PATCH] Implement LSP Text Document Synchronization (#10941) --- Cargo.lock | 4 +- crates/nu-lsp/Cargo.toml | 3 +- crates/nu-lsp/src/diagnostics.rs | 147 +++++++ crates/nu-lsp/src/lib.rs | 416 ++++++++++++------ crates/nu-lsp/src/notification.rs | 185 ++++++++ .../src/engine/state_working_set.rs | 9 + src/main.rs | 2 +- tests/fixtures/lsp/diagnostics/var.nu | 1 + 8 files changed, 620 insertions(+), 147 deletions(-) create mode 100644 crates/nu-lsp/src/diagnostics.rs create mode 100644 crates/nu-lsp/src/notification.rs create mode 100644 tests/fixtures/lsp/diagnostics/var.nu diff --git a/Cargo.lock b/Cargo.lock index 10e634de64..fc9f2a193b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2312,8 +2312,7 @@ dependencies = [ [[package]] name = "lsp-server" version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b52dccdf3302eefab8c8a1273047f0a3c3dca4b527c8458d00c09484c8371928" +source = "git+https://github.com/schrieveslaach/rust-analyzer.git?branch=cancelable-initialization#f95d538d3af1eac9266b898c8bed384c1498edfc" dependencies = [ "crossbeam-channel", "log", @@ -2972,6 +2971,7 @@ name = "nu-lsp" version = "0.87.1" dependencies = [ "assert-json-diff", + "crossbeam-channel", "lsp-server", "lsp-types", "miette", diff --git a/crates/nu-lsp/Cargo.toml b/crates/nu-lsp/Cargo.toml index c4da63efce..930a4a9ec8 100644 --- a/crates/nu-lsp/Cargo.toml +++ b/crates/nu-lsp/Cargo.toml @@ -14,8 +14,9 @@ nu-protocol = { path = "../nu-protocol", version = "0.87.1" } reedline = { version = "0.26" } +crossbeam-channel = "0.5.8" lsp-types = "0.94.1" -lsp-server = "0.7.4" +lsp-server = { version = "0.7.4", git = "https://github.com/schrieveslaach/rust-analyzer.git", branch = "cancelable-initialization" } miette = "5.10" ropey = "1.6.1" serde = "1.0" diff --git a/crates/nu-lsp/src/diagnostics.rs b/crates/nu-lsp/src/diagnostics.rs new file mode 100644 index 0000000000..a6126e02c0 --- /dev/null +++ b/crates/nu-lsp/src/diagnostics.rs @@ -0,0 +1,147 @@ +use lsp_types::{ + notification::{Notification, PublishDiagnostics}, + Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Url, +}; +use miette::{IntoDiagnostic, Result}; +use nu_parser::parse; +use nu_protocol::{ + engine::{EngineState, StateWorkingSet}, + eval_const::create_nu_constant, + Span, Value, NU_VARIABLE_ID, +}; + +use crate::LanguageServer; + +impl LanguageServer { + pub(crate) fn publish_diagnostics_for_file( + &self, + uri: Url, + engine_state: &mut EngineState, + ) -> Result<()> { + 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 Ok(nu_const) = create_nu_constant(engine_state, Span::unknown()) else { + return Ok(()); + }; + engine_state.set_variable_const_val(NU_VARIABLE_ID, nu_const); + + let mut working_set = StateWorkingSet::new(engine_state); + + let Some((rope_of_file, file_path)) = self.rope(&uri) else { + return Ok(()); + }; + + let contents = rope_of_file.bytes().collect::>(); + let offset = working_set.next_span_start(); + parse( + &mut working_set, + Some(&file_path.to_string_lossy()), + &contents, + false, + ); + + let mut diagnostics = PublishDiagnosticsParams { + uri, + diagnostics: Vec::new(), + version: None, + }; + + for err in working_set.parse_errors.iter() { + let message = err.to_string(); + + diagnostics.diagnostics.push(Diagnostic { + range: Self::span_to_range(&err.span(), rope_of_file, offset), + severity: Some(DiagnosticSeverity::ERROR), + message, + ..Default::default() + }); + } + + self.connection + .sender + .send(lsp_server::Message::Notification( + lsp_server::Notification::new(PublishDiagnostics::METHOD.to_string(), diagnostics), + )) + .into_diagnostic() + } +} + +#[cfg(test)] +mod tests { + use assert_json_diff::assert_json_eq; + use lsp_types::Url; + use nu_test_support::fs::fixtures; + + use crate::tests::{initialize_language_server, open, update}; + + #[test] + fn publish_diagnostics_variable_does_not_exists() { + let (client_connection, _recv) = initialize_language_server(); + + let mut script = fixtures(); + script.push("lsp"); + script.push("diagnostics"); + script.push("var.nu"); + let script = Url::from_file_path(script).unwrap(); + + let notification = open(&client_connection, script.clone()); + + assert_json_eq!( + notification, + serde_json::json!({ + "method": "textDocument/publishDiagnostics", + "params": { + "uri": script, + "diagnostics": [{ + "range": { + "start": { "line": 0, "character": 6 }, + "end": { "line": 0, "character": 30 } + }, + "message": "Variable not found.", + "severity": 1 + }] + } + }) + ); + } + + #[test] + fn publish_diagnostics_fixed_unknown_variable() { + let (client_connection, _recv) = initialize_language_server(); + + let mut script = fixtures(); + script.push("lsp"); + script.push("diagnostics"); + script.push("var.nu"); + let script = Url::from_file_path(script).unwrap(); + + open(&client_connection, script.clone()); + let notification = update( + &client_connection, + script.clone(), + String::from("$env"), + Some(lsp_types::Range { + start: lsp_types::Position { + line: 0, + character: 6, + }, + end: lsp_types::Position { + line: 0, + character: 30, + }, + }), + ); + + assert_json_eq!( + notification, + serde_json::json!({ + "method": "textDocument/publishDiagnostics", + "params": { + "uri": script, + "diagnostics": [] + } + }) + ); + } +} diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 9c94f44d24..5eb184b078 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -1,11 +1,19 @@ -use std::{fs::File, io::Cursor, sync::Arc}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; 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, + OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit, Url, }; use miette::{IntoDiagnostic, Result}; use nu_cli::NuCompleter; @@ -17,6 +25,9 @@ use nu_protocol::{ use reedline::Completer; use ropey::Rope; +mod diagnostics; +mod notification; + #[derive(Debug)] enum Id { Variable(VarId), @@ -27,6 +38,7 @@ enum Id { pub struct LanguageServer { connection: Connection, io_threads: Option, + ropes: BTreeMap, } impl LanguageServer { @@ -42,11 +54,19 @@ impl LanguageServer { Ok(Self { connection, io_threads, + ropes: BTreeMap::new(), }) } - pub fn serve_requests(self, engine_state: EngineState) -> Result<()> { + pub fn serve_requests( + mut self, + engine_state: EngineState, + ctrlc: Arc, + ) -> 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()), @@ -56,10 +76,22 @@ impl LanguageServer { let _initialization_params = self .connection - .initialize(server_capabilities) + .initialize_while(server_capabilities, || !ctrlc.load(Ordering::SeqCst)) .into_diagnostic()?; - for msg in &self.connection.receiver { + while !ctrlc.load(Ordering::SeqCst) { + 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 @@ -71,25 +103,39 @@ impl LanguageServer { } 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, - )?; + 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; } - HoverRequest::METHOD => { - self.handle_lsp_request(&mut engine_state, request, Self::hover)?; - } - Completion::METHOD => { - self.handle_lsp_request(&mut engine_state, request, Self::complete)?; - } - _ => {} - } + }; + + self.connection + .sender + .send(Message::Response(resp)) + .into_diagnostic()?; } Message::Response(_) => {} - Message::Notification(_) => {} + 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)?; + } + } } } @@ -101,41 +147,36 @@ impl LanguageServer { } fn handle_lsp_request( - &self, engine_state: &mut EngineState, req: lsp_server::Request, - param_handler: H, - ) -> Result<()> + mut param_handler: H, + ) -> Response where P: serde::de::DeserializeOwned, - H: Fn(&mut EngineState, &P) -> Option, + H: FnMut(&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, - }, + 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, - }), - }, - } - }; - - self.connection - .sender - .send(Message::Response(resp)) - .into_diagnostic() + 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 { @@ -158,79 +199,84 @@ impl LanguageServer { lsp_types::Range { start, end } } - fn lsp_position_to_location(position: &lsp_types::Position, rope_of_file: &Rope) -> usize { + 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, - file_path: &str, - file: &[u8], + path: &Path, + file: &Rope, 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 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 item in flattened { - if location >= item.0.start && location < item.0.end { - match &item.1 { + + 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, item.0)); + return Some((Id::Variable(*var_id), offset, span)); } FlatShape::InternalCall(decl_id) => { - return Some((Id::Declaration(*decl_id), offset, item.0)); + return Some((Id::Declaration(*decl_id), offset, span)); } - _ => return Some((Id::Value(item.1), offset, item.0)), + _ => return Some((Id::Value(shape), offset, span)), } } } 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()?; + fn rope<'a, 'b: 'a>(&'b self, file_url: &Url) -> Option<(&'a Rope, &'a PathBuf)> { + let file_path = file_url.to_file_path().ok()?; - engine_state.start_in_file(Some(file_path)); + 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)?; + + // TODO: AsPath thingy + engine_state.start_in_file(Some(&path.to_string_lossy())); let working_set = StateWorkingSet::new(engine_state); - Ok((file, working_set)) + 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 = 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 (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, - &file_path, - &file, - Self::lsp_position_to_location( - ¶ms.text_document_position_params.position, - &rope_of_file, - ), + path, + file, + Self::lsp_position_to_location(¶ms.text_document_position_params.position, file), )?; match id { @@ -242,7 +288,7 @@ impl LanguageServer { 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), + range: Self::span_to_range(span, file, *file_start), })); } } @@ -261,11 +307,7 @@ impl LanguageServer { .text_document .uri .clone(), - range: Self::span_to_range( - &var.declaration_span, - &rope_of_file, - *file_start, - ), + range: Self::span_to_range(&var.declaration_span, file, *file_start), })); } } @@ -275,30 +317,20 @@ impl LanguageServer { None } - fn hover(engine_state: &mut EngineState, params: &HoverParams) -> Option { + 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 = 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 (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, - &file_path, - &file, - Self::lsp_position_to_location( - ¶ms.text_document_position_params.position, - &rope_of_file, - ), + path, + file, + Self::lsp_position_to_location(¶ms.text_document_position_params.position, file), )?; match id { @@ -489,27 +521,23 @@ impl LanguageServer { } 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 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 (rope_of_file, _, _) = self.read_in_file( + engine_state, + ¶ms.text_document_position.text_document.uri, + )?; 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); + 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 @@ -545,15 +573,18 @@ mod tests { use super::*; use assert_json_diff::assert_json_eq; use lsp_types::{ - notification::{Exit, Initialized, Notification}, + notification::{ + DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification, + }, request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown}, - CompletionParams, GotoDefinitionParams, InitializeParams, InitializedParams, - TextDocumentIdentifier, TextDocumentPositionParams, Url, + CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, + GotoDefinitionParams, InitializeParams, InitializedParams, TextDocumentContentChangeEvent, + TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, Url, }; use nu_test_support::fs::{fixtures, root}; use std::sync::mpsc::Receiver; - fn initialize_language_server() -> (Connection, 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(); @@ -562,7 +593,7 @@ mod tests { 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)) + send.send(lsp_server.serve_requests(engine_state, Arc::new(AtomicBool::new(false)))) }); client_connection @@ -651,16 +682,91 @@ mod tests { .receiver .recv_timeout(std::time::Duration::from_secs(2)) .unwrap(); + let result = if let Message::Response(response) = resp { + response.result + } else { + panic!() + }; - assert!(matches!( - resp, - Message::Response(response) if response.result.is_none() - )); + assert_json_eq!(result, serde_json::json!(null)); } - fn goto_definition(uri: Url, line: u32, character: u32) -> Message { - let (client_connection, _recv) = initialize_language_server(); + pub fn open(client_connection: &Connection, uri: Url) -> lsp_server::Notification { + let text = std::fs::read_to_string(uri.to_file_path().unwrap()).unwrap(); + 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(), + })) + .unwrap(); + + let notification = client_connection + .receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(); + + if let Message::Notification(n) = notification { + n + } else { + panic!(); + } + } + + 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 { @@ -686,13 +792,17 @@ mod tests { #[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(); - let resp = goto_definition(script.clone(), 2, 12); + open(&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 { @@ -713,13 +823,17 @@ mod tests { #[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(); - let resp = goto_definition(script.clone(), 4, 1); + open(&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 { @@ -740,13 +854,17 @@ mod tests { #[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(); - let resp = goto_definition(script.clone(), 1, 14); + open(&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 { @@ -765,9 +883,7 @@ mod tests { ); } - fn hover(uri: Url, line: u32, character: u32) -> Message { - let (client_connection, _recv) = initialize_language_server(); - + pub fn hover(client_connection: &Connection, uri: Url, line: u32, character: u32) -> Message { client_connection .sender .send(Message::Request(lsp_server::Request { @@ -792,13 +908,17 @@ mod tests { #[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(); - let resp = hover(script.clone(), 2, 0); + open(&client_connection, script.clone()); + + let resp = hover(&client_connection, script.clone(), 2, 0); let result = if let Message::Response(response) = resp { response.result } else { @@ -815,13 +935,17 @@ mod tests { #[test] fn hover_on_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(); - let resp = hover(script.clone(), 3, 0); + open(&client_connection, script.clone()); + + let resp = hover(&client_connection, script.clone(), 3, 0); let result = if let Message::Response(response) = resp { response.result } else { @@ -839,9 +963,7 @@ mod tests { ); } - fn complete(uri: Url, line: u32, character: u32) -> Message { - let (client_connection, _recv) = initialize_language_server(); - + fn complete(client_connection: &Connection, uri: Url, line: u32, character: u32) -> Message { client_connection .sender .send(Message::Request(lsp_server::Request { @@ -868,13 +990,17 @@ mod tests { #[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(); - let resp = complete(script, 2, 9); + open(&client_connection, script.clone()); + + let resp = complete(&client_connection, script, 2, 9); let result = if let Message::Response(response) = resp { response.result } else { @@ -900,13 +1026,17 @@ mod tests { #[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(); - let resp = complete(script, 0, 8); + open(&client_connection, script.clone()); + + let resp = complete(&client_connection, script, 0, 8); let result = if let Message::Response(response) = resp { response.result } else { diff --git a/crates/nu-lsp/src/notification.rs b/crates/nu-lsp/src/notification.rs new file mode 100644 index 0000000000..e8dce97c7d --- /dev/null +++ b/crates/nu-lsp/src/notification.rs @@ -0,0 +1,185 @@ +use lsp_types::{ + notification::{ + DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification, + }, + DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, Url, +}; +use ropey::Rope; + +use crate::LanguageServer; + +impl LanguageServer { + pub(crate) fn handle_lsp_notification( + &mut self, + notification: lsp_server::Notification, + ) -> Option { + match notification.method.as_str() { + DidOpenTextDocument::METHOD => Self::handle_notification_payload::< + DidOpenTextDocumentParams, + _, + >(notification, |param| { + if let Ok(file_path) = param.text_document.uri.to_file_path() { + let rope = Rope::from_str(¶m.text_document.text); + self.ropes.insert(file_path, rope); + Some(param.text_document.uri) + } else { + None + } + }), + DidChangeTextDocument::METHOD => { + Self::handle_notification_payload::( + notification, + |params| self.update_rope(params), + ) + } + DidCloseTextDocument::METHOD => Self::handle_notification_payload::< + DidCloseTextDocumentParams, + _, + >(notification, |param| { + if let Ok(file_path) = param.text_document.uri.to_file_path() { + self.ropes.remove(&file_path); + } + None + }), + _ => None, + } + } + + fn handle_notification_payload( + notification: lsp_server::Notification, + mut param_handler: H, + ) -> Option + where + P: serde::de::DeserializeOwned, + H: FnMut(P) -> Option, + { + if let Ok(params) = serde_json::from_value::

(notification.params) { + param_handler(params) + } else { + None + } + } + + fn update_rope(&mut self, params: DidChangeTextDocumentParams) -> Option { + if let Ok(file_path) = params.text_document.uri.to_file_path() { + for content_change in params.content_changes.into_iter() { + let entry = self.ropes.entry(file_path.clone()); + match (content_change.range, content_change.range) { + (Some(range), _) => { + entry.and_modify(|rope| { + let start = Self::lsp_position_to_location(&range.start, rope); + let end = Self::lsp_position_to_location(&range.end, rope); + + rope.remove(start..end); + rope.insert(start, &content_change.text); + }); + } + (None, None) => { + entry.and_modify(|r| *r = Rope::from_str(&content_change.text)); + } + _ => {} + } + } + + Some(params.text_document.uri) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use assert_json_diff::assert_json_eq; + use lsp_server::Message; + use lsp_types::{Range, Url}; + use nu_test_support::fs::fixtures; + + use crate::tests::{hover, initialize_language_server, open, update}; + + #[test] + fn hover_on_command_after_full_content_change() { + 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(&client_connection, script.clone()); + update( + &client_connection, + script.clone(), + String::from( + r#"# Renders some updated greeting message +def hello [] {} + +hello"#, + ), + None, + ); + + 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": "```\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 updated greeting message\n" + } + }) + ); + } + + #[test] + fn hover_on_command_after_partial_content_change() { + 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(&client_connection, script.clone()); + update( + &client_connection, + script.clone(), + String::from("# Renders some updated greeting message"), + Some(Range { + start: lsp_types::Position { + line: 0, + character: 0, + }, + end: lsp_types::Position { + line: 0, + character: 31, + }, + }), + ); + + 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": "```\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 updated greeting message\n" + } + }) + ); + } +} diff --git a/crates/nu-protocol/src/engine/state_working_set.rs b/crates/nu-protocol/src/engine/state_working_set.rs index 37cc7ff956..679f201b48 100644 --- a/crates/nu-protocol/src/engine/state_working_set.rs +++ b/crates/nu-protocol/src/engine/state_working_set.rs @@ -317,6 +317,15 @@ impl<'a> StateWorkingSet<'a> { self.num_virtual_paths() - 1 } + pub fn get_span_for_filename(&self, filename: &str) -> Option { + let (file_id, ..) = self + .files() + .enumerate() + .find(|(_, (fname, _, _))| fname == filename)?; + + Some(self.get_span_for_file(file_id)) + } + pub fn get_span_for_file(&self, file_id: usize) -> Span { let result = self .files() diff --git a/src/main.rs b/src/main.rs index ce0b4c024e..c60018a33a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,7 +193,7 @@ fn main() -> Result<()> { } if parsed_nu_cli_args.lsp { - return LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state); + return LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state, ctrlc); } // IDE commands diff --git a/tests/fixtures/lsp/diagnostics/var.nu b/tests/fixtures/lsp/diagnostics/var.nu new file mode 100644 index 0000000000..a72431139a --- /dev/null +++ b/tests/fixtures/lsp/diagnostics/var.nu @@ -0,0 +1 @@ +print $this_var_is_not_defined