diff --git a/Cargo.lock b/Cargo.lock index 65d98c9fb1..68234d6b4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1964,6 +1964,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "fnv" version = "1.0.7" @@ -3195,16 +3204,26 @@ dependencies = [ ] [[package]] -name = "lsp-types" -version = "0.95.1" +name = "lsp-textdocument" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" +checksum = "d8dc223af95101fe950a871d4d567b6f98a1ecfcee5861f4b57644581aaa980d" +dependencies = [ + "lsp-types", + "serde_json", +] + +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" dependencies = [ "bitflags 1.3.2", + "fluent-uri", "serde", "serde_json", "serde_repr", - "url", ] [[package]] @@ -3924,6 +3943,7 @@ dependencies = [ "assert-json-diff", "crossbeam-channel", "lsp-server", + "lsp-textdocument", "lsp-types", "miette", "nu-cli", @@ -3933,9 +3953,9 @@ dependencies = [ "nu-protocol", "nu-test-support", "reedline", - "ropey", "serde", "serde_json", + "url", ] [[package]] @@ -6247,16 +6267,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ropey" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" -dependencies = [ - "smallvec", - "str_indices", -] - [[package]] name = "roxmltree" version = "0.20.0" @@ -7015,12 +7025,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str_indices" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" - [[package]] name = "streaming-decompression" version = "0.1.2" @@ -7800,7 +7804,6 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ce52af8b67..33a8f8f883 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,8 @@ log = "0.4" lru = "0.12" lscolors = { version = "0.17", default-features = false } lsp-server = "0.7.5" -lsp-types = { version = "0.95.0", features = ["proposed"] } +lsp-types = { version = "0.97.0", features = ["proposed"] } +lsp-textdocument = "0.4.0" mach2 = "0.4" md5 = { version = "0.10", package = "md-5" } miette = "7.3" @@ -139,10 +140,8 @@ rand_chacha = "0.3.1" ratatui = "0.26" rayon = "1.10" reedline = "0.38.0" -regex = "1.9.5" rmp = "0.8" rmp-serde = "1.3" -ropey = "1.6.1" roxmltree = "0.20" rstest = { version = "0.23", default-features = false } rusqlite = "0.31" @@ -330,4 +329,4 @@ bench = false # Run individual benchmarks like `cargo bench -- ` e.g. `cargo bench -- parse` [[bench]] name = "benchmarks" -harness = false \ No newline at end of file +harness = false diff --git a/crates/nu-lsp/Cargo.toml b/crates/nu-lsp/Cargo.toml index 41f3467257..829b1bd152 100644 --- a/crates/nu-lsp/Cargo.toml +++ b/crates/nu-lsp/Cargo.toml @@ -15,12 +15,13 @@ nu-protocol = { path = "../nu-protocol", version = "0.101.1" } reedline = { workspace = true } crossbeam-channel = { workspace = true } -lsp-types = { workspace = true } lsp-server = { workspace = true } +lsp-types = { workspace = true } +lsp-textdocument = { workspace = true } miette = { workspace = true } -ropey = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +url = { workspace = true } [dev-dependencies] nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.101.1" } @@ -30,4 +31,4 @@ nu-test-support = { path = "../nu-test-support", version = "0.101.1" } assert-json-diff = "2.0" [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/crates/nu-lsp/src/diagnostics.rs b/crates/nu-lsp/src/diagnostics.rs index 53b47bd2f0..5f94f021a2 100644 --- a/crates/nu-lsp/src/diagnostics.rs +++ b/crates/nu-lsp/src/diagnostics.rs @@ -1,41 +1,24 @@ use crate::LanguageServer; use lsp_types::{ notification::{Notification, PublishDiagnostics}, - Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Url, + Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Uri, }; use miette::{IntoDiagnostic, Result}; -use nu_parser::parse; -use nu_protocol::{ - engine::{EngineState, StateWorkingSet}, - Span, Value, -}; +use nu_protocol::Value; impl LanguageServer { - pub(crate) fn publish_diagnostics_for_file( - &self, - uri: Url, - engine_state: &mut EngineState, - ) -> Result<()> { + pub(crate) fn publish_diagnostics_for_file(&mut self, uri: Uri) -> Result<()> { + let mut engine_state = self.engine_state.clone(); 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())); engine_state.generate_nu_constant(); - let mut working_set = StateWorkingSet::new(engine_state); - - let Some((rope_of_file, file_path)) = self.rope(&uri) else { + let Some((_, offset, working_set, file)) = + self.update_engine_state(&mut engine_state, &uri) + else { return Ok(()); }; - let contents = rope_of_file.bytes().collect::>(); - let offset = working_set.next_span_start(); - working_set.files.push(file_path.into(), Span::unknown())?; - parse( - &mut working_set, - Some(&file_path.to_string_lossy()), - &contents, - false, - ); - let mut diagnostics = PublishDiagnosticsParams { uri, diagnostics: Vec::new(), @@ -46,12 +29,7 @@ impl LanguageServer { let message = err.to_string(); diagnostics.diagnostics.push(Diagnostic { - range: Self::span_to_range( - &err.span(), - rope_of_file, - offset, - &self.position_encoding, - ), + range: Self::span_to_range(&err.span(), file, offset), severity: Some(DiagnosticSeverity::ERROR), message, ..Default::default() @@ -70,20 +48,20 @@ impl LanguageServer { #[cfg(test)] mod tests { use assert_json_diff::assert_json_eq; - use lsp_types::Url; use nu_test_support::fs::fixtures; + use crate::path_to_uri; use crate::tests::{initialize_language_server, open_unchecked, update}; #[test] fn publish_diagnostics_variable_does_not_exists() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); let notification = open_unchecked(&client_connection, script.clone()); @@ -108,13 +86,13 @@ mod tests { #[test] fn publish_diagnostics_fixed_unknown_variable() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); let notification = update( diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 3e12a24b2a..a159447c7c 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -1,27 +1,28 @@ #![doc = include_str!("../README.md")] use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; +use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_types::{ request::{Completion, GotoDefinition, HoverRequest, Request}, CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, - MarkupContent, MarkupKind, OneOf, Position, PositionEncodingKind, Range, ServerCapabilities, - TextDocumentSyncKind, TextEdit, Url, + MarkupContent, MarkupKind, OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit, + Uri, }; use miette::{IntoDiagnostic, Result}; use nu_cli::{NuCompleter, SuggestionKind}; use nu_parser::{flatten_block, parse, FlatShape}; use nu_protocol::{ - engine::{CachedFile, EngineState, Stack, StateWorkingSet}, + ast::Block, + engine::{EngineState, Stack, StateWorkingSet}, DeclId, Span, Value, VarId, }; -use ropey::Rope; -use serde_json::json; use std::{ - collections::BTreeMap, path::{Path, PathBuf}, + str::FromStr, sync::Arc, time::Duration, }; +use url::Url; mod diagnostics; mod notification; @@ -36,40 +37,49 @@ enum Id { pub struct LanguageServer { connection: Connection, io_threads: Option, - ropes: BTreeMap, - position_encoding: PositionEncodingKind, + docs: TextDocuments, + engine_state: EngineState, +} + +pub fn path_to_uri

(path: P) -> Uri +where + P: AsRef, +{ + Uri::from_str( + Url::from_file_path(path) + .expect("Failed to convert path to Url") + .as_str(), + ) + .expect("Failed to convert Url to lsp_types::Uri.") +} + +pub fn uri_to_path(uri: &Uri) -> PathBuf { + Url::from_str(uri.as_str()) + .expect("Failed to convert Uri to Url") + .to_file_path() + .expect("Failed to convert Url to path") } impl LanguageServer { - pub fn initialize_stdio_connection() -> Result { + pub fn initialize_stdio_connection(engine_state: EngineState) -> Result { let (connection, io_threads) = Connection::stdio(); - Self::initialize_connection(connection, Some(io_threads)) + Self::initialize_connection(connection, Some(io_threads), engine_state) } fn initialize_connection( connection: Connection, io_threads: Option, + engine_state: EngineState, ) -> Result { Ok(Self { connection, io_threads, - ropes: BTreeMap::new(), - position_encoding: PositionEncodingKind::UTF16, + docs: TextDocuments::new(), + engine_state, }) } - fn get_offset_encoding(&self, initialization_params: serde_json::Value) -> String { - initialization_params - .pointer("/capabilities/offsetEncoding/0") - .unwrap_or( - initialization_params - .pointer("/capabilities/offset_encoding/0") - .unwrap_or(&json!("utf-16")), - ) - .to_string() - } - - pub fn serve_requests(mut self, engine_state: EngineState) -> Result<()> { + pub fn serve_requests(mut self) -> Result<()> { let server_capabilities = serde_json::to_value(ServerCapabilities { text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind( TextDocumentSyncKind::INCREMENTAL, @@ -81,16 +91,14 @@ impl LanguageServer { }) .expect("Must be serializable"); - let initialization_params = self + let _ = self .connection .initialize_while(server_capabilities, || { - !engine_state.signals().interrupted() + !self.engine_state.signals().interrupted() }) .into_diagnostic()?; - self.position_encoding = - PositionEncodingKind::from(self.get_offset_encoding(initialization_params)); - while !engine_state.signals().interrupted() { + while !self.engine_state.signals().interrupted() { let msg = match self .connection .receiver @@ -113,23 +121,16 @@ impl LanguageServer { 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), - ), + GotoDefinition::METHOD => { + Self::handle_lsp_request(request, |params| self.goto_definition(params)) + } + HoverRequest::METHOD => { + Self::handle_lsp_request(request, |params| self.hover(params)) + } + Completion::METHOD => { + Self::handle_lsp_request(request, |params| self.complete(params)) + } _ => { continue; } @@ -143,8 +144,7 @@ impl LanguageServer { 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)?; + self.publish_diagnostics_for_file(updated_file)?; } } } @@ -157,21 +157,38 @@ impl LanguageServer { Ok(()) } - fn handle_lsp_request( - engine_state: &mut EngineState, - req: lsp_server::Request, - mut param_handler: H, - ) -> Response + pub fn update_engine_state<'a>( + &mut self, + engine_state: &'a mut EngineState, + uri: &Uri, + ) -> Option<(Arc, usize, StateWorkingSet<'a>, &FullTextDocument)> { + let mut working_set = StateWorkingSet::new(engine_state); + let file = self.docs.get_document(uri)?; + let file_path = uri_to_path(uri); + let file_path_str = file_path.to_str()?; + let contents = file.get_content(None).as_bytes(); + let _ = working_set.files.push(file_path.clone(), Span::unknown()); + let block = parse(&mut working_set, Some(file_path_str), contents, false); + let offset = working_set + .get_span_for_filename(file_path_str) + .unwrap_or_else(|| panic!("Failed at get_span_for_filename {}", file_path_str)) + .start; + // TODO: merge delta back to engine_state? + // self.engine_state.merge_delta(working_set.render()); + Some((block, offset, working_set, file)) + } + + fn handle_lsp_request(req: lsp_server::Request, mut param_handler: H) -> Response where P: serde::de::DeserializeOwned, - H: FnMut(&mut EngineState, &P) -> Option, + H: FnMut(&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) + param_handler(¶ms) .and_then(|response| serde_json::to_value(response).ok()) .unwrap_or(serde_json::Value::Null), ), @@ -190,97 +207,17 @@ impl LanguageServer { } } - fn span_to_range( - span: &Span, - rope_of_file: &Rope, - offset: usize, - position_encoding: &PositionEncodingKind, - ) -> Range { - let start = Self::lsp_byte_offset_to_utf_cu_position( - span.start.saturating_sub(offset), - rope_of_file, - position_encoding, - ); - let end = Self::lsp_byte_offset_to_utf_cu_position( - span.end.saturating_sub(offset), - rope_of_file, - position_encoding, - ); + fn span_to_range(span: &Span, file: &FullTextDocument, offset: usize) -> Range { + let start = file.position_at(span.start.saturating_sub(offset) as u32); + let end = file.position_at(span.end.saturating_sub(offset) as u32); Range { start, end } } - fn lsp_byte_offset_to_utf_cu_position( - offset: usize, - rope_of_file: &Rope, - position_encoding: &PositionEncodingKind, - ) -> Position { - let line = rope_of_file.try_byte_to_line(offset).unwrap_or(0); - match position_encoding.as_str() { - "\"utf-8\"" => { - let character = offset - rope_of_file.line_to_byte(line); - Position { - line: line as u32, - character: character as u32, - } - } - _ => { - let character = rope_of_file.char_to_utf16_cu(rope_of_file.byte_to_char(offset)) - - rope_of_file.char_to_utf16_cu(rope_of_file.line_to_char(line)); - Position { - line: line as u32, - character: character as u32, - } - } - } - } - - fn utf16_cu_position_to_char(rope_of_file: &Rope, position: &Position) -> usize { - let line_utf_idx = - rope_of_file.char_to_utf16_cu(rope_of_file.line_to_char(position.line as usize)); - rope_of_file.utf16_cu_to_char(line_utf_idx + position.character as usize) - } - - pub fn lsp_position_to_location( - position: &Position, - rope_of_file: &Rope, - position_encoding: &PositionEncodingKind, - ) -> usize { - match position_encoding.as_str() { - "\"utf-8\"" => rope_of_file.byte_to_char( - rope_of_file.line_to_byte(position.line as usize) + position.character as usize, - ), - _ => Self::utf16_cu_position_to_char(rope_of_file, position), - } - } - - fn lsp_position_to_byte_offset(&self, position: &Position, rope_of_file: &Rope) -> usize { - match self.position_encoding.as_str() { - "\"utf-8\"" => { - rope_of_file.line_to_byte(position.line as usize) + position.character as usize - } - _ => rope_of_file - .try_char_to_byte(Self::utf16_cu_position_to_char(rope_of_file, position)) - .expect("Character index out of range!"), - } - } - fn find_id( - working_set: &mut StateWorkingSet, - path: &Path, - file: &Rope, + flattened: Vec<(Span, FlatShape)>, location: usize, + offset: 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 _ = working_set - .files - .push(file_path.as_ref().into(), Span::unknown()); - 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 { @@ -299,122 +236,88 @@ impl LanguageServer { 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>( - &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 rope_file_from_cached_file(&mut self, cached_file: &CachedFile) -> Result<(Url, &Rope), ()> { - let uri = Url::from_file_path(&*cached_file.name)?; - let rope_of_file = self.ropes.entry(uri.to_file_path()?).or_insert_with(|| { - let raw_string = String::from_utf8_lossy(&cached_file.content); - Rope::from_str(&raw_string) - }); - Ok((uri, rope_of_file)) - } - - 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_byte_offset(¶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) { - let position_encoding = self.position_encoding.clone(); - let (uri, rope_of_file) = - self.rope_file_from_cached_file(cached_file).ok()?; - return Some(GotoDefinitionResponse::Scalar(Location { - uri, - range: Self::span_to_range( - span, - rope_of_file, - cached_file.covered_span.start, - &position_encoding, - ), - })); - } - } - } + fn get_location_by_span(&self, working_set: &StateWorkingSet, span: &Span) -> Option { + for cached_file in working_set.files() { + if cached_file.covered_span.contains(span.start) { + let path = Path::new(&*cached_file.name); + if !(path.exists() && path.is_file()) { + return None; + } + let target_uri = path_to_uri(path); + if let Some(doc) = self.docs.get_document(&target_uri) { + return Some(Location { + uri: target_uri, + range: Self::span_to_range(span, doc, cached_file.covered_span.start), + }); + } else { + // in case where the document is not opened yet, typically included by `nu -I` + let temp_doc = FullTextDocument::new( + "Unk".to_string(), + 0, + String::from_utf8((*cached_file.content).to_vec()).expect("Invalid UTF-8"), + ); + return Some(Location { + uri: target_uri, + range: Self::span_to_range(span, &temp_doc, 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) - { - let position_encoding = self.position_encoding.clone(); - let (uri, rope_of_file) = - self.rope_file_from_cached_file(cached_file).ok()?; - return Some(GotoDefinitionResponse::Scalar(Location { - uri, - range: Self::span_to_range( - &var.declaration_span, - rope_of_file, - cached_file.covered_span.start, - &position_encoding, - ), - })); - } - } - } - Id::Value(_) => {} } None } - fn hover(&mut self, engine_state: &mut EngineState, params: &HoverParams) -> Option { + fn goto_definition(&mut self, params: &GotoDefinitionParams) -> Option { + let mut engine_state = self.engine_state.clone(); 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 path_uri = params + .text_document_position_params + .text_document + .uri + .to_owned(); + let (block, file_offset, working_set, file) = + self.update_engine_state(&mut engine_state, &path_uri)?; + let flattened = flatten_block(&working_set, &block); + let (id, _, _) = Self::find_id( + flattened, + file.offset_at(params.text_document_position_params.position) as usize, + file_offset, )?; + let span = match id { + Id::Declaration(decl_id) => { + let block_id = working_set.get_decl(decl_id).block_id()?; + working_set.get_block(block_id).span + } + Id::Variable(var_id) => { + let var = working_set.get_variable(var_id); + Some(var.declaration_span) + } + _ => None, + }?; + Some(GotoDefinitionResponse::Scalar( + self.get_location_by_span(&working_set, &span)?, + )) + } + + fn hover(&mut self, params: &HoverParams) -> Option { + let mut engine_state = self.engine_state.clone(); + 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 path_uri = params + .text_document_position_params + .text_document + .uri + .to_owned(); + let (block, file_offset, working_set, file) = + self.update_engine_state(&mut engine_state, &path_uri)?; + let flattened = flatten_block(&working_set, &block); let (id, _, _) = Self::find_id( - &mut working_set, - path, - file, - self.lsp_position_to_byte_offset(¶ms.text_document_position_params.position, file), + flattened, + file.offset_at(params.text_document_position_params.position) as usize, + file_offset, )?; match id { @@ -615,26 +518,15 @@ 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 (rope_of_file, _, _) = self.read_in_file( - engine_state, - ¶ms.text_document_position.text_document.uri, - )?; + fn complete(&mut self, params: &CompletionParams) -> Option { + let path_uri = params.text_document_position.text_document.uri.to_owned(); + let file = self.docs.get_document(&path_uri)?; let mut completer = - NuCompleter::new(Arc::new(engine_state.clone()), Arc::new(Stack::new())); + NuCompleter::new(Arc::new(self.engine_state.clone()), Arc::new(Stack::new())); - let location = - self.lsp_position_to_byte_offset(¶ms.text_document_position.position, rope_of_file); - let results = - completer.fetch_completions_at(&rope_of_file.to_string()[..location], location); + let location = file.offset_at(params.text_document_position.position) as usize; + let results = completer.fetch_completions_at(&file.get_content(None)[..location], location); if results.is_empty() { None } else { @@ -691,26 +583,23 @@ mod tests { }, request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown}, CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, - GotoDefinitionParams, InitializeParams, InitializedParams, PartialResultParams, + GotoDefinitionParams, InitializeParams, InitializedParams, PartialResultParams, Position, TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem, - TextDocumentPositionParams, Url, WorkDoneProgressParams, + TextDocumentPositionParams, WorkDoneProgressParams, }; use nu_test_support::fs::{fixtures, root}; use std::sync::mpsc::Receiver; - pub fn initialize_language_server( - client_offset_encoding: Option>, - ) -> (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(); + let engine_state = nu_cmd_lang::create_default_context(); + let engine_state = nu_command::add_shell_command_context(engine_state); + let lsp_server = + LanguageServer::initialize_connection(server_connection, None, engine_state).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)) - }); + std::thread::spawn(move || send.send(lsp_server.serve_requests())); client_connection .sender @@ -719,7 +608,6 @@ mod tests { method: Initialize::METHOD.to_string(), params: serde_json::to_value(InitializeParams { capabilities: lsp_types::ClientCapabilities { - offset_encoding: client_offset_encoding, ..Default::default() }, ..Default::default() @@ -745,7 +633,7 @@ mod tests { #[test] fn shutdown_on_request() { - let (client_connection, recv) = initialize_language_server(None); + let (client_connection, recv) = initialize_language_server(); client_connection .sender @@ -771,7 +659,7 @@ mod tests { #[test] fn goto_definition_for_none_existing_file() { - let (client_connection, _recv) = initialize_language_server(None); + let (client_connection, _recv) = initialize_language_server(); let mut none_existent_path = root(); none_existent_path.push("none-existent.nu"); @@ -784,7 +672,7 @@ mod tests { params: serde_json::to_value(GotoDefinitionParams { text_document_position_params: TextDocumentPositionParams { text_document: TextDocumentIdentifier { - uri: Url::from_file_path(none_existent_path).unwrap(), + uri: path_to_uri(&none_existent_path), }, position: Position { line: 0, @@ -811,16 +699,15 @@ mod tests { assert_json_eq!(result, serde_json::json!(null)); } - pub fn open_unchecked(client_connection: &Connection, uri: Url) -> lsp_server::Notification { + pub fn open_unchecked(client_connection: &Connection, uri: Uri) -> lsp_server::Notification { open(client_connection, uri).unwrap() } pub fn open( client_connection: &Connection, - uri: Url, + uri: Uri, ) -> Result { - let text = - std::fs::read_to_string(uri.to_file_path().unwrap()).map_err(|e| e.to_string())?; + let text = std::fs::read_to_string(uri_to_path(&uri)).map_err(|e| e.to_string())?; client_connection .sender @@ -852,7 +739,7 @@ mod tests { pub fn update( client_connection: &Connection, - uri: Url, + uri: Uri, text: String, range: Option, ) -> lsp_server::Notification { @@ -863,7 +750,7 @@ mod tests { method: DidChangeTextDocument::METHOD.to_string(), params: serde_json::to_value(DidChangeTextDocumentParams { text_document: lsp_types::VersionedTextDocumentIdentifier { - uri, + uri: uri.clone(), version: 2, }, content_changes: vec![TextDocumentContentChangeEvent { @@ -891,7 +778,7 @@ mod tests { fn goto_definition( client_connection: &Connection, - uri: Url, + uri: Uri, line: u32, character: u32, ) -> Message { @@ -920,13 +807,13 @@ mod tests { #[test] fn goto_definition_of_variable() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -940,24 +827,24 @@ mod tests { assert_json_eq!( result, serde_json::json!({ - "uri": script, - "range": { - "start": { "line": 0, "character": 4 }, - "end": { "line": 0, "character": 12 } - } + "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(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -971,29 +858,28 @@ mod tests { assert_json_eq!( result, serde_json::json!({ - "uri": script, - "range": { - "start": { "line": 0, "character": 17 }, - "end": { "line": 2, "character": 1 } - } + "uri": script, + "range": { + "start": { "line": 0, "character": 17 }, + "end": { "line": 2, "character": 1 } + } }) ); } #[test] - fn goto_definition_of_command_utf8() { - let (client_connection, _recv) = - initialize_language_server(Some(vec!["utf-8".to_string(), "utf-16".to_string()])); + fn goto_definition_of_command_unicode() { + let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("goto"); script.push("command_unicode.nu"); - let script = Url::from_file_path(script).unwrap(); + let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); - let resp = goto_definition(&client_connection, script.clone(), 4, 1); + let resp = goto_definition(&client_connection, script.clone(), 4, 2); let result = if let Message::Response(response) = resp { response.result } else { @@ -1003,55 +889,24 @@ mod tests { assert_json_eq!( result, serde_json::json!({ - "uri": script, - "range": { - "start": { "line": 0, "character": 28 }, - "end": { "line": 2, "character": 1 } - } - }) - ); - } - - #[test] - fn goto_definition_of_command_utf16() { - let (client_connection, _recv) = initialize_language_server(None); - - let mut script = fixtures(); - script.push("lsp"); - script.push("goto"); - script.push("command_unicode.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": 19 }, - "end": { "line": 2, "character": 1 } - } + "uri": script, + "range": { + "start": { "line": 0, "character": 19 }, + "end": { "line": 2, "character": 1 } + } }) ); } #[test] fn goto_definition_of_command_parameter() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -1065,16 +920,16 @@ mod tests { assert_json_eq!( result, serde_json::json!({ - "uri": script, - "range": { - "start": { "line": 0, "character": 11 }, - "end": { "line": 0, "character": 15 } - } + "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 { + pub fn hover(client_connection: &Connection, uri: Uri, line: u32, character: u32) -> Message { client_connection .sender .send(Message::Request(lsp_server::Request { @@ -1099,13 +954,13 @@ mod tests { #[test] fn hover_on_variable() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -1126,13 +981,13 @@ mod tests { #[test] fn hover_on_custom_command() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -1146,7 +1001,7 @@ mod tests { assert_json_eq!( result, serde_json::json!({ - "contents": { + "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" } @@ -1156,13 +1011,13 @@ mod tests { #[test] fn hover_on_str_join() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -1176,7 +1031,7 @@ mod tests { assert_json_eq!( result, serde_json::json!({ - "contents": { + "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" } @@ -1184,7 +1039,7 @@ mod tests { ); } - fn complete(client_connection: &Connection, uri: Url, line: u32, character: u32) -> Message { + fn complete(client_connection: &Connection, uri: Uri, line: u32, character: u32) -> Message { client_connection .sender .send(Message::Request(lsp_server::Request { @@ -1211,13 +1066,13 @@ mod tests { #[test] fn complete_on_variable() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -1231,30 +1086,30 @@ mod tests { 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 - } + { + "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(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -1268,71 +1123,31 @@ mod tests { 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 - } + { + "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_utf8_line() { - let (client_connection, _recv) = - initialize_language_server(Some(vec!["utf-8".to_string()])); + fn complete_command_with_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_command_with_utf16_line() { - let (client_connection, _recv) = - initialize_language_server(Some(vec!["utf-16".to_string()])); - - let mut script = fixtures(); - script.push("lsp"); - script.push("completion"); - script.push("utf_pipeline.nu"); - let script = Url::from_file_path(script).unwrap(); + let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -1346,31 +1161,31 @@ mod tests { assert_json_eq!( result, serde_json::json!([ - { - "label": "str trim", - "detail": "Trim whitespace or specific character.", - "textEdit": { - "range": { - "start": { "line": 0, "character": 8 }, - "end": { "line": 0, "character": 13 }, - }, - "newText": "str trim" - }, - "kind": 3 - } + { + "label": "str trim", + "detail": "Trim whitespace or specific character.", + "textEdit": { + "range": { + "start": { "line": 0, "character": 8 }, + "end": { "line": 0, "character": 13 }, + }, + "newText": "str trim" + }, + "kind": 3 + } ]) ); } #[test] fn complete_keyword() { - let (client_connection, _recv) = initialize_language_server(None); + 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(); + let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -1387,14 +1202,14 @@ mod tests { { "label": "overlay", "textEdit": { - "newText": "overlay", - "range": { - "start": { "character": 0, "line": 0 }, - "end": { "character": 2, "line": 0 } - } - }, - "kind": 14 - }, + "newText": "overlay", + "range": { + "start": { "character": 0, "line": 0 }, + "end": { "character": 2, "line": 0 } + } + }, + "kind": 14 + }, ]) ); } diff --git a/crates/nu-lsp/src/notification.rs b/crates/nu-lsp/src/notification.rs index a3e563b468..33ec9ed5de 100644 --- a/crates/nu-lsp/src/notification.rs +++ b/crates/nu-lsp/src/notification.rs @@ -1,10 +1,7 @@ use lsp_types::{ - notification::{ - DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification, - }, - DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, Url, + notification::{DidChangeTextDocument, DidOpenTextDocument, DidSaveTextDocument, Notification}, + DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, Uri, }; -use ropey::Rope; use crate::LanguageServer; @@ -12,107 +9,52 @@ impl LanguageServer { pub(crate) fn handle_lsp_notification( &mut self, notification: lsp_server::Notification, - ) -> Option { + ) -> Option { + self.docs + .listen(notification.method.as_str(), ¬ification.params); 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 - } - }), + DidOpenTextDocument::METHOD => { + let params: DidOpenTextDocumentParams = + serde_json::from_value(notification.params.clone()) + .expect("Expect receive DidOpenTextDocumentParams"); + Some(params.text_document.uri) + } + DidSaveTextDocument::METHOD => { + let params: DidSaveTextDocumentParams = + serde_json::from_value(notification.params.clone()) + .expect("Expect receive DidSaveTextDocumentParams"); + Some(params.text_document.uri) + } DidChangeTextDocument::METHOD => { - Self::handle_notification_payload::( - notification, - |params| self.update_rope(params), - ) + let params: DidChangeTextDocumentParams = + serde_json::from_value(notification.params.clone()) + .expect("Expect receive DidChangeTextDocumentParams"); + Some(params.text_document.uri) } - 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 { - Some(range) => { - entry.and_modify(|rope| { - let start = Self::lsp_position_to_location( - &range.start, - rope, - &self.position_encoding, - ); - let end = Self::lsp_position_to_location( - &range.end, - rope, - &self.position_encoding, - ); - - rope.remove(start..end); - rope.insert(start, &content_change.text); - }); - } - 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 lsp_types::Range; use nu_test_support::fs::fixtures; + use crate::path_to_uri; use crate::tests::{hover, initialize_language_server, open, open_unchecked, update}; #[test] fn hover_correct_documentation_on_let() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); @@ -136,13 +78,13 @@ mod tests { #[test] fn hover_on_command_after_full_content_change() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); update( @@ -177,13 +119,13 @@ hello"#, #[test] fn hover_on_command_after_partial_content_change() { - let (client_connection, _recv) = initialize_language_server(None); + 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 script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); update( @@ -222,13 +164,13 @@ hello"#, #[test] fn open_document_with_utf_char() { - let (client_connection, _recv) = initialize_language_server(None); + let (client_connection, _recv) = initialize_language_server(); let mut script = fixtures(); script.push("lsp"); script.push("notifications"); script.push("issue_11522.nu"); - let script = Url::from_file_path(script).unwrap(); + let script = path_to_uri(&script); let result = open(&client_connection, script); diff --git a/src/main.rs b/src/main.rs index 32a602707e..cb86557ac2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -460,7 +460,7 @@ fn main() -> Result<()> { ); } - LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state)? + LanguageServer::initialize_stdio_connection(engine_state)?.serve_requests()? } else if let Some(commands) = parsed_nu_cli_args.commands.clone() { run_commands( &mut engine_state,