diff --git a/crates/nu-lsp/src/ast.rs b/crates/nu-lsp/src/ast.rs index e0925cab47..62d7b85f56 100644 --- a/crates/nu-lsp/src/ast.rs +++ b/crates/nu-lsp/src/ast.rs @@ -1,3 +1,4 @@ +use crate::Id; use nu_protocol::{ ast::{ Argument, Block, Call, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern, @@ -6,9 +7,7 @@ use nu_protocol::{ engine::StateWorkingSet, Span, }; -use std::{path::PathBuf, sync::Arc}; - -use crate::Id; +use std::sync::Arc; /// similar to flatten_block, but allows extra map function pub fn ast_flat_map<'a, T, F>( @@ -243,7 +242,7 @@ fn try_find_id_in_mod( id_ref: Option<&Id>, ) -> Option<(Id, Span)> { let call_name = working_set.get_span_contents(call.head); - if call_name != "module".as_bytes() && call_name != "export module".as_bytes() { + if call_name != b"module" && call_name != b"export module" { return None; }; let check_location = |span: &Span| location.map_or(true, |pos| span.contains(*pos)); @@ -283,7 +282,7 @@ fn try_find_id_in_use( id: Option<&Id>, ) -> Option<(Id, Span)> { let call_name = working_set.get_span_contents(call.head); - if call_name != "use".as_bytes() { + if call_name != b"use" { return None; } let find_by_name = |name: &[u8]| match id { @@ -307,7 +306,7 @@ fn try_find_id_in_use( let get_module_id = |span: Span| { let span = strip_quotes(span, working_set); let name = String::from_utf8_lossy(working_set.get_span_contents(span)); - let path = PathBuf::from(name.as_ref()); + let path = std::path::PathBuf::from(name.as_ref()); let stem = path.file_stem().and_then(|fs| fs.to_str()).unwrap_or(&name); let found_id = Id::Module(working_set.find_module(stem.as_bytes())?); id.map_or(true, |id_r| found_id == *id_r) @@ -420,7 +419,7 @@ fn find_id_in_expr( } /// find the leaf node at the given location from ast -pub fn find_id( +pub(crate) fn find_id( ast: &Arc, working_set: &StateWorkingSet, location: &usize, @@ -452,11 +451,9 @@ fn find_reference_by_id_in_expr( .filter_map(|arg| arg.expr()) .flat_map(recur) .collect(); - if let Id::Declaration(decl_id) = id { - if *decl_id == call.decl_id { - occurs.push(call.head); - return Some(occurs); - } + if matches!(id, Id::Declaration(decl_id) if call.decl_id == *decl_id) { + occurs.push(call.head); + return Some(occurs); } if let Some((_, span_found)) = try_find_id_in_def(call, working_set, None, Some(id)) .or(try_find_id_in_mod(call, working_set, None, Some(id))) @@ -470,7 +467,11 @@ fn find_reference_by_id_in_expr( } } -pub fn find_reference_by_id(ast: &Arc, working_set: &StateWorkingSet, id: &Id) -> Vec { +pub(crate) fn find_reference_by_id( + ast: &Arc, + working_set: &StateWorkingSet, + id: &Id, +) -> Vec { ast_flat_map(ast, working_set, &|e| { find_reference_by_id_in_expr(e, working_set, id) }) diff --git a/crates/nu-lsp/src/diagnostics.rs b/crates/nu-lsp/src/diagnostics.rs index 632521b024..70e94a7da2 100644 --- a/crates/nu-lsp/src/diagnostics.rs +++ b/crates/nu-lsp/src/diagnostics.rs @@ -49,11 +49,10 @@ impl LanguageServer { #[cfg(test)] mod tests { - use assert_json_diff::assert_json_eq; - use nu_test_support::fs::fixtures; - use crate::path_to_uri; use crate::tests::{initialize_language_server, open_unchecked, update}; + use assert_json_diff::assert_json_eq; + use nu_test_support::fs::fixtures; #[test] fn publish_diagnostics_variable_does_not_exists() { diff --git a/crates/nu-lsp/src/goto.rs b/crates/nu-lsp/src/goto.rs index a1a5527675..15e607477e 100644 --- a/crates/nu-lsp/src/goto.rs +++ b/crates/nu-lsp/src/goto.rs @@ -4,7 +4,10 @@ use nu_protocol::engine::StateWorkingSet; use nu_protocol::Span; impl LanguageServer { - pub fn find_definition_span_by_id(working_set: &StateWorkingSet, id: &Id) -> Option { + pub(crate) fn find_definition_span_by_id( + working_set: &StateWorkingSet, + id: &Id, + ) -> Option { match id { Id::Declaration(decl_id) => { let block_id = working_set.get_decl(*decl_id).block_id()?; @@ -22,7 +25,7 @@ impl LanguageServer { } } - pub fn goto_definition( + pub(crate) fn goto_definition( &mut self, params: &GotoDefinitionParams, ) -> Option { @@ -54,8 +57,8 @@ mod tests { use crate::tests::{initialize_language_server, open_unchecked}; use assert_json_diff::assert_json_eq; use lsp_server::{Connection, Message}; - use lsp_types::request::{GotoDefinition, Request}; use lsp_types::{ + request::{GotoDefinition, Request}, GotoDefinitionParams, PartialResultParams, Position, TextDocumentIdentifier, TextDocumentPositionParams, Uri, WorkDoneProgressParams, }; diff --git a/crates/nu-lsp/src/hints.rs b/crates/nu-lsp/src/hints.rs index 44f4ab66b7..a3ee410cf4 100644 --- a/crates/nu-lsp/src/hints.rs +++ b/crates/nu-lsp/src/hints.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use crate::ast::{ast_flat_map, expr_flat_map}; use crate::{span_to_range, LanguageServer}; use lsp_textdocument::FullTextDocument; @@ -12,6 +10,7 @@ use nu_protocol::{ engine::StateWorkingSet, Type, }; +use std::sync::Arc; fn type_short_name(t: &Type) -> String { match t { @@ -145,12 +144,11 @@ fn extract_inlay_hints_from_expression( } impl LanguageServer { - pub fn get_inlay_hints(&mut self, params: &InlayHintParams) -> Option> { + pub(crate) fn get_inlay_hints(&mut self, params: &InlayHintParams) -> Option> { Some(self.inlay_hints.get(¶ms.text_document.uri)?.clone()) } - pub fn extract_inlay_hints( - &self, + pub(crate) fn extract_inlay_hints( working_set: &StateWorkingSet, block: &Arc, offset: usize, @@ -164,17 +162,15 @@ impl LanguageServer { #[cfg(test)] mod tests { - use assert_json_diff::assert_json_eq; - use lsp_types::request::Request; - use nu_test_support::fs::fixtures; - use crate::path_to_uri; use crate::tests::{initialize_language_server, open_unchecked}; + use assert_json_diff::assert_json_eq; use lsp_server::{Connection, Message}; use lsp_types::{ - request::InlayHintRequest, InlayHintParams, Position, Range, TextDocumentIdentifier, Uri, - WorkDoneProgressParams, + request::{InlayHintRequest, Request}, + InlayHintParams, Position, Range, TextDocumentIdentifier, Uri, WorkDoneProgressParams, }; + use nu_test_support::fs::fixtures; fn send_inlay_hint_request(client_connection: &Connection, uri: Uri) -> Message { client_connection diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 788afd9e0e..14862fb6c5 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -1,33 +1,31 @@ #![doc = include_str!("../README.md")] -use ast::find_id; -use crossbeam_channel::{Receiver, Sender}; use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_types::{ request::{self, Request}, - CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit, - Hover, HoverContents, HoverParams, InlayHint, Location, MarkupContent, MarkupKind, OneOf, - Position, Range, ReferencesOptions, RenameOptions, ServerCapabilities, TextDocumentSyncKind, - TextEdit, Uri, WorkDoneProgressOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities, + CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, + CompletionResponse, CompletionTextEdit, Documentation, Hover, HoverContents, HoverParams, + InlayHint, Location, MarkupContent, MarkupKind, OneOf, Position, Range, ReferencesOptions, + RenameOptions, ServerCapabilities, TextDocumentSyncKind, TextEdit, Uri, + WorkDoneProgressOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; use miette::{miette, IntoDiagnostic, Result}; use nu_cli::{NuCompleter, SuggestionKind}; -use nu_parser::parse; use nu_protocol::{ ast::Block, - engine::{CachedFile, EngineState, Stack, StateDelta, StateWorkingSet}, - DeclId, ModuleId, Span, Type, Value, VarId, + engine::{CachedFile, Command, EngineState, Stack, StateDelta, StateWorkingSet}, + DeclId, ModuleId, Span, Type, VarId, }; -use std::{collections::BTreeMap, sync::Mutex}; use std::{ + collections::BTreeMap, path::{Path, PathBuf}, str::FromStr, sync::Arc, + sync::Mutex, time::Duration, }; use symbols::SymbolCache; -use url::Url; use workspace::{InternalMessage, RangePerDoc}; mod ast; @@ -39,7 +37,7 @@ mod symbols; mod workspace; #[derive(Debug, Clone, Eq, PartialEq)] -pub enum Id { +pub(crate) enum Id { Variable(VarId), Declaration(DeclId), Value(Type), @@ -56,30 +54,33 @@ pub struct LanguageServer { workspace_folders: BTreeMap, /// for workspace wide requests occurrences: BTreeMap>, - channels: Option<(Sender, Arc>)>, + channels: Option<( + crossbeam_channel::Sender, + Arc>, + )>, /// set to true when text changes need_parse: bool, /// cache `StateDelta` to avoid repeated parsing cached_state_delta: Option, } -pub fn path_to_uri(path: impl AsRef) -> Uri { +pub(crate) fn path_to_uri(path: impl AsRef) -> Uri { Uri::from_str( - Url::from_file_path(path) + url::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()) +pub(crate) fn uri_to_path(uri: &Uri) -> PathBuf { + url::Url::from_str(uri.as_str()) .expect("Failed to convert Uri to Url") .to_file_path() .expect("Failed to convert Url to path") } -pub fn span_to_range(span: &Span, file: &FullTextDocument, offset: usize) -> Range { +pub(crate) 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 } @@ -251,14 +252,14 @@ impl LanguageServer { } /// Send a cancel message to a running bg thread - pub fn cancel_background_thread(&mut self) { + pub(crate) fn cancel_background_thread(&mut self) { if let Some((sender, _)) = &self.channels { sender.send(true).ok(); } } /// Check results from background thread - pub fn handle_internal_messages(&mut self) -> Result { + pub(crate) fn handle_internal_messages(&mut self) -> Result { let mut reset = false; if let Some((_, receiver)) = &self.channels { for im in receiver.try_iter() { @@ -286,10 +287,13 @@ impl LanguageServer { Ok(reset) } - pub fn new_engine_state(&self) -> EngineState { + pub(crate) fn new_engine_state(&self) -> EngineState { let mut engine_state = self.initial_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.add_env_var( + "PWD".into(), + nu_protocol::Value::test_string(cwd.to_string_lossy()), + ); // merge the cached `StateDelta` if text not changed if !self.need_parse { engine_state @@ -303,7 +307,7 @@ impl LanguageServer { engine_state } - pub fn parse_and_find<'a>( + pub(crate) fn parse_and_find<'a>( &mut self, engine_state: &'a mut EngineState, uri: &Uri, @@ -321,12 +325,12 @@ impl LanguageServer { .get_document(uri) .ok_or_else(|| miette!("\nFailed to get document"))?; let location = file.offset_at(pos) as usize + file_span.start; - let (id, span) = find_id(&block, &working_set, &location) + let (id, span) = ast::find_id(&block, &working_set, &location) .ok_or_else(|| miette!("\nFailed to find current name"))?; Ok((working_set, id, span, file_span.start)) } - pub fn parse_file<'a>( + pub(crate) fn parse_file<'a>( &mut self, engine_state: &'a mut EngineState, uri: &Uri, @@ -339,10 +343,11 @@ impl LanguageServer { 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 block = nu_parser::parse(&mut working_set, Some(file_path_str), contents, false); let span = working_set.get_span_for_filename(file_path_str)?; if need_hints { - let file_inlay_hints = self.extract_inlay_hints(&working_set, &block, span.start, file); + let file_inlay_hints = + Self::extract_inlay_hints(&working_set, &block, span.start, file); self.inlay_hints.insert(uri.clone(), file_inlay_hints); } if self.need_parse { @@ -418,6 +423,141 @@ impl LanguageServer { } } + fn get_decl_description(decl: &dyn Command) -> String { + let mut description = String::new(); + + // First description + description.push_str(&format!("{}\n", decl.description().replace('\r', ""))); + + // Additional description + if !decl.extra_description().is_empty() { + description.push_str(&format!("\n{}\n", decl.extra_description())); + } + + // Usage + description.push_str("-----\n### Usage \n```nu\n"); + let signature = decl.signature(); + description.push_str(&format!(" {}", signature.name)); + if !signature.named.is_empty() { + description.push_str(" {flags}"); + } + for required_arg in &signature.required_positional { + description.push_str(&format!(" <{}>", required_arg.name)); + } + for optional_arg in &signature.optional_positional { + description.push_str(&format!(" <{}?>", optional_arg.name)); + } + if let Some(arg) = &signature.rest_positional { + description.push_str(&format!(" <...{}>", arg.name)); + } + description.push_str("\n```\n"); + + // Flags + if !signature.named.is_empty() { + description.push_str("\n### Flags\n\n"); + let mut first = true; + for named in &signature.named { + if first { + first = false; + } else { + description.push('\n'); + } + description.push_str(" "); + if let Some(short_flag) = &named.short { + description.push_str(&format!("`-{short_flag}`")); + } + if !named.long.is_empty() { + if named.short.is_some() { + description.push_str(", "); + } + description.push_str(&format!("`--{}`", named.long)); + } + if let Some(arg) = &named.arg { + description.push_str(&format!(" `<{}>`", arg.to_type())); + } + if !named.desc.is_empty() { + description.push_str(&format!(" - {}", named.desc)); + } + description.push('\n'); + } + description.push('\n'); + } + + // Parameters + if !signature.required_positional.is_empty() + || !signature.optional_positional.is_empty() + || signature.rest_positional.is_some() + { + description.push_str("\n### Parameters\n\n"); + let mut first = true; + for required_arg in &signature.required_positional { + if first { + first = false; + } else { + description.push('\n'); + } + description.push_str(&format!( + " `{}: {}`", + required_arg.name, + required_arg.shape.to_type() + )); + if !required_arg.desc.is_empty() { + description.push_str(&format!(" - {}", required_arg.desc)); + } + description.push('\n'); + } + for optional_arg in &signature.optional_positional { + if first { + first = false; + } else { + description.push('\n'); + } + description.push_str(&format!( + " `{}: {}`", + optional_arg.name, + optional_arg.shape.to_type() + )); + if !optional_arg.desc.is_empty() { + description.push_str(&format!(" - {}", optional_arg.desc)); + } + description.push('\n'); + } + if let Some(arg) = &signature.rest_positional { + if !first { + description.push('\n'); + } + description.push_str(&format!(" `...{}: {}`", arg.name, arg.shape.to_type())); + if !arg.desc.is_empty() { + description.push_str(&format!(" - {}", arg.desc)); + } + description.push('\n'); + } + description.push('\n'); + } + + // Input/output types + if !signature.input_output_types.is_empty() { + description.push_str("\n### Input/output types\n"); + description.push_str("\n```nu\n"); + for input_output in &signature.input_output_types { + description.push_str(&format!(" {} | {}\n", input_output.0, input_output.1)); + } + description.push_str("\n```\n"); + } + + // Examples + if !decl.examples().is_empty() { + description.push_str("### Example(s)\n"); + for example in decl.examples() { + description.push_str(&format!( + " {}\n```nu\n {}\n```\n", + example.description, example.example + )); + } + } + description + } + fn hover(&mut self, params: &HoverParams) -> Option { let mut engine_state = self.new_engine_state(); @@ -448,150 +588,20 @@ impl LanguageServer { match id { Id::Variable(var_id) => { let var = working_set.get_variable(var_id); - let contents = - format!("{} `{}`", if var.mutable { "mutable " } else { "" }, var.ty); + let contents = format!( + "{}{} `{}`", + if var.const_val.is_some() { + "const " + } else { + "" + }, + if var.mutable { "mutable " } else { "" }, + var.ty, + ); markdown_hover(contents) } Id::Declaration(decl_id) => { - let decl = working_set.get_decl(decl_id); - - let mut description = String::new(); - - // First description - description.push_str(&format!("{}\n", decl.description().replace('\r', ""))); - - // Additional description - if !decl.extra_description().is_empty() { - description.push_str(&format!("\n{}\n", decl.extra_description())); - } - - // Usage - description.push_str("-----\n### Usage \n```nu\n"); - let signature = decl.signature(); - description.push_str(&format!(" {}", signature.name)); - if !signature.named.is_empty() { - description.push_str(" {flags}"); - } - for required_arg in &signature.required_positional { - description.push_str(&format!(" <{}>", required_arg.name)); - } - for optional_arg in &signature.optional_positional { - description.push_str(&format!(" <{}?>", optional_arg.name)); - } - if let Some(arg) = &signature.rest_positional { - description.push_str(&format!(" <...{}>", arg.name)); - } - description.push_str("\n```\n"); - - // Flags - if !signature.named.is_empty() { - description.push_str("\n### Flags\n\n"); - let mut first = true; - for named in &signature.named { - if first { - first = false; - } else { - description.push('\n'); - } - description.push_str(" "); - if let Some(short_flag) = &named.short { - description.push_str(&format!("`-{short_flag}`")); - } - if !named.long.is_empty() { - if named.short.is_some() { - description.push_str(", "); - } - description.push_str(&format!("`--{}`", named.long)); - } - if let Some(arg) = &named.arg { - description.push_str(&format!(" `<{}>`", arg.to_type())); - } - if !named.desc.is_empty() { - description.push_str(&format!(" - {}", named.desc)); - } - description.push('\n'); - } - description.push('\n'); - } - - // Parameters - if !signature.required_positional.is_empty() - || !signature.optional_positional.is_empty() - || signature.rest_positional.is_some() - { - description.push_str("\n### Parameters\n\n"); - let mut first = true; - for required_arg in &signature.required_positional { - if first { - first = false; - } else { - description.push('\n'); - } - description.push_str(&format!( - " `{}: {}`", - required_arg.name, - required_arg.shape.to_type() - )); - if !required_arg.desc.is_empty() { - description.push_str(&format!(" - {}", required_arg.desc)); - } - description.push('\n'); - } - for optional_arg in &signature.optional_positional { - if first { - first = false; - } else { - description.push('\n'); - } - description.push_str(&format!( - " `{}: {}`", - optional_arg.name, - optional_arg.shape.to_type() - )); - if !optional_arg.desc.is_empty() { - description.push_str(&format!(" - {}", optional_arg.desc)); - } - description.push('\n'); - } - if let Some(arg) = &signature.rest_positional { - if !first { - description.push('\n'); - } - description.push_str(&format!( - " `...{}: {}`", - arg.name, - arg.shape.to_type() - )); - if !arg.desc.is_empty() { - description.push_str(&format!(" - {}", arg.desc)); - } - description.push('\n'); - } - description.push('\n'); - } - - // Input/output types - if !signature.input_output_types.is_empty() { - description.push_str("\n### Input/output types\n"); - description.push_str("\n```nu\n"); - for input_output in &signature.input_output_types { - description - .push_str(&format!(" {} | {}\n", input_output.0, input_output.1)); - } - description.push_str("\n```\n"); - } - - // Examples - if !decl.examples().is_empty() { - description.push_str("### Example(s)\n"); - for example in decl.examples() { - description.push_str(&format!( - " {}\n```nu\n {}\n```\n", - example.description, example.example - )); - } - } - markdown_hover(description) + markdown_hover(Self::get_decl_description(working_set.get_decl(decl_id))) } Id::Module(module_id) => { let mut description = String::new(); @@ -612,40 +622,62 @@ impl LanguageServer { let docs = self.docs.lock().ok()?; let file = docs.get_document(&path_uri)?; - let mut completer = NuCompleter::new( - Arc::new(self.initial_engine_state.clone()), - Arc::new(Stack::new()), - ); + let engine_state = Arc::new(self.initial_engine_state.clone()); + let mut completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new())); 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 { - Some(CompletionResponse::Array( - results - .into_iter() - .map(|r| { - let mut start = params.text_document_position.position; - start.character -= (r.suggestion.span.end - r.suggestion.span.start) as u32; + (!results.is_empty()).then_some(CompletionResponse::Array( + results + .into_iter() + .map(|r| { + let mut start = params.text_document_position.position; + start.character -= (r.suggestion.span.end - r.suggestion.span.start) as u32; + let decl_id = r.kind.clone().and_then(|kind| { + matches!(kind, SuggestionKind::Command(_)) + .then_some(engine_state.find_decl(r.suggestion.value.as_bytes(), &[])?) + }); - CompletionItem { - label: r.suggestion.value.clone(), - detail: r.suggestion.description, - kind: Self::lsp_completion_item_kind(r.kind), - text_edit: Some(CompletionTextEdit::Edit(TextEdit { - range: Range { - start, - end: params.text_document_position.position, - }, - new_text: r.suggestion.value, - })), - ..Default::default() - } - }) - .collect(), - )) - } + CompletionItem { + label: r.suggestion.value.clone(), + label_details: r + .kind + .clone() + .map(|kind| match kind { + SuggestionKind::Type(t) => t.to_string(), + SuggestionKind::Command(cmd) => cmd.to_string(), + }) + .map(|s| CompletionItemLabelDetails { + detail: None, + description: Some(s), + }), + detail: r.suggestion.description, + documentation: r + .suggestion + .extra + .map(|ex| ex.join("\n")) + .or(decl_id.map(|decl_id| { + Self::get_decl_description(engine_state.get_decl(decl_id)) + })) + .map(|value| { + Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value, + }) + }), + kind: Self::lsp_completion_item_kind(r.kind), + text_edit: Some(CompletionTextEdit::Edit(TextEdit { + range: Range { + start, + end: params.text_document_position.position, + }, + new_text: r.suggestion.value, + })), + ..Default::default() + } + }) + .collect(), + )) } fn lsp_completion_item_kind( @@ -681,8 +713,9 @@ mod tests { }; use nu_test_support::fs::fixtures; use std::sync::mpsc::Receiver; + use std::time::Duration; - pub fn initialize_language_server( + pub(crate) fn initialize_language_server( params: Option, ) -> (Connection, Receiver>) { use std::sync::mpsc; @@ -713,7 +746,7 @@ mod tests { let _initialize_response = client_connection .receiver - .recv_timeout(std::time::Duration::from_secs(2)) + .recv_timeout(Duration::from_secs(2)) .unwrap(); (client_connection, recv) @@ -739,17 +772,17 @@ mod tests { })) .unwrap(); - assert!(recv - .recv_timeout(std::time::Duration::from_secs(2)) - .unwrap() - .is_ok()); + assert!(recv.recv_timeout(Duration::from_secs(2)).unwrap().is_ok()); } - pub fn open_unchecked(client_connection: &Connection, uri: Uri) -> lsp_server::Notification { + pub(crate) fn open_unchecked( + client_connection: &Connection, + uri: Uri, + ) -> lsp_server::Notification { open(client_connection, uri).unwrap() } - pub fn open( + pub(crate) fn open( client_connection: &Connection, uri: Uri, ) -> Result { @@ -783,7 +816,7 @@ mod tests { } } - pub fn update( + pub(crate) fn update( client_connection: &Connection, uri: Uri, text: String, @@ -822,7 +855,7 @@ mod tests { } } - pub fn send_hover_request( + pub(crate) fn send_hover_request( client_connection: &Connection, uri: Uri, line: u32, @@ -846,7 +879,7 @@ mod tests { client_connection .receiver - .recv_timeout(std::time::Duration::from_secs(2)) + .recv_timeout(Duration::from_secs(2)) .unwrap() } @@ -991,7 +1024,7 @@ mod tests { client_connection .receiver - .recv_timeout(std::time::Duration::from_secs(2)) + .recv_timeout(Duration::from_secs(2)) .unwrap() } @@ -1014,20 +1047,18 @@ mod tests { panic!() }; - assert_json_eq!( - result, - serde_json::json!([ + assert_json_include!( + actual: result, + expected: serde_json::json!([ { "label": "$greeting", + "labelDetails": { "description": "string" }, "textEdit": { - "newText": "$greeting", - "range": { - "start": { "character": 5, "line": 2 }, - "end": { "character": 9, "line": 2 } - } - }, - "kind": 6 - } + "newText": "$greeting", + "range": { "start": { "character": 5, "line": 2 }, "end": { "character": 9, "line": 2 } } + }, + "kind": 6 + } ]) ); } @@ -1051,21 +1082,17 @@ mod tests { panic!() }; - assert_json_eq!( - result, - serde_json::json!([ + assert_json_include!( + actual: result, + expected: 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 - } + "textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, }, + "newText": "config nu" + }, + "kind": 3 + } ]) ); } @@ -1089,21 +1116,19 @@ mod tests { panic!() }; - assert_json_eq!( - result, - serde_json::json!([ + assert_json_include!( + actual: result, + expected: serde_json::json!([ { "label": "str trim", + "labelDetails": { "description": "built-in" }, "detail": "Trim whitespace or specific character.", "textEdit": { - "range": { - "start": { "line": 0, "character": 8 }, - "end": { "line": 0, "character": 13 }, - }, - "newText": "str trim" - }, - "kind": 3 - } + "range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 }, }, + "newText": "str trim" + }, + "kind": 3 + } ]) ); } @@ -1132,15 +1157,13 @@ mod tests { expected: serde_json::json!([ { "label": "overlay", + "labelDetails": { "description": "keyword" }, "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 399d1237f6..d64e5412ac 100644 --- a/crates/nu-lsp/src/notification.rs +++ b/crates/nu-lsp/src/notification.rs @@ -1,4 +1,4 @@ -use lsp_server::{Message, RequestId, Response, ResponseError}; +use crate::LanguageServer; use lsp_types::{ notification::{ DidChangeTextDocument, DidChangeWorkspaceFolders, DidCloseTextDocument, @@ -8,8 +8,6 @@ use lsp_types::{ DidOpenTextDocumentParams, ProgressParams, ProgressParamsValue, ProgressToken, Uri, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressReport, }; - -use crate::LanguageServer; use miette::{IntoDiagnostic, Result}; impl LanguageServer { @@ -57,7 +55,7 @@ impl LanguageServer { } } - pub fn send_progress_notification( + pub(crate) fn send_progress_notification( &self, token: ProgressToken, value: WorkDoneProgress, @@ -74,7 +72,7 @@ impl LanguageServer { .into_diagnostic() } - pub fn send_progress_begin(&self, token: ProgressToken, title: String) -> Result<()> { + pub(crate) fn send_progress_begin(&self, token: ProgressToken, title: String) -> Result<()> { self.send_progress_notification( token, WorkDoneProgress::Begin(WorkDoneProgressBegin { @@ -86,7 +84,7 @@ impl LanguageServer { ) } - pub fn send_progress_report( + pub(crate) fn send_progress_report( &self, token: ProgressToken, percentage: u32, @@ -102,20 +100,29 @@ impl LanguageServer { ) } - pub fn send_progress_end(&self, token: ProgressToken, message: Option) -> Result<()> { + pub(crate) fn send_progress_end( + &self, + token: ProgressToken, + message: Option, + ) -> Result<()> { self.send_progress_notification( token, WorkDoneProgress::End(WorkDoneProgressEnd { message }), ) } - pub fn send_error_message(&self, id: RequestId, code: i32, message: String) -> Result<()> { + pub(crate) fn send_error_message( + &self, + id: lsp_server::RequestId, + code: i32, + message: String, + ) -> Result<()> { self.connection .sender - .send(Message::Response(Response { + .send(lsp_server::Message::Response(lsp_server::Response { id, result: None, - error: Some(ResponseError { + error: Some(lsp_server::ResponseError { code, message, data: None, @@ -128,15 +135,14 @@ impl LanguageServer { #[cfg(test)] mod tests { - use assert_json_diff::assert_json_eq; - use lsp_server::Message; - use lsp_types::Range; - use nu_test_support::fs::fixtures; - use crate::path_to_uri; use crate::tests::{ initialize_language_server, open, open_unchecked, send_hover_request, update, }; + use assert_json_diff::assert_json_eq; + use lsp_server::Message; + use lsp_types::Range; + use nu_test_support::fs::fixtures; #[test] fn hover_correct_documentation_on_let() { diff --git a/crates/nu-lsp/src/symbols.rs b/crates/nu-lsp/src/symbols.rs index c2952f34db..480acf6d40 100644 --- a/crates/nu-lsp/src/symbols.rs +++ b/crates/nu-lsp/src/symbols.rs @@ -1,21 +1,20 @@ -use std::collections::{BTreeMap, HashSet}; -use std::hash::{Hash, Hasher}; - use crate::{path_to_uri, span_to_range, uri_to_path, Id, LanguageServer}; use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_types::{ DocumentSymbolParams, DocumentSymbolResponse, Location, Range, SymbolInformation, SymbolKind, Uri, WorkspaceSymbolParams, WorkspaceSymbolResponse, }; -use nu_parser::parse; -use nu_protocol::ModuleId; use nu_protocol::{ engine::{CachedFile, EngineState, StateWorkingSet}, - DeclId, Span, VarId, + DeclId, ModuleId, Span, VarId, }; use nucleo_matcher::pattern::{AtomKind, CaseMatching, Normalization, Pattern}; use nucleo_matcher::{Config, Matcher, Utf32Str}; -use std::{cmp::Ordering, path::Path}; +use std::{ + cmp::Ordering, + collections::{BTreeMap, HashSet}, + hash::{Hash, Hasher}, +}; /// Struct stored in cache, uri not included #[derive(Clone, Debug, Eq, PartialEq)] @@ -69,7 +68,7 @@ impl Symbol { } /// Cache symbols for each opened file to avoid repeated parsing -pub struct SymbolCache { +pub(crate) struct SymbolCache { /// Fuzzy matcher for symbol names matcher: Matcher, /// File Uri --> Symbols @@ -187,7 +186,7 @@ impl SymbolCache { .get_document_content(uri, None) .expect("Failed to get_document_content!") .as_bytes(); - parse( + nu_parser::parse( &mut working_set, Some( uri_to_path(uri) @@ -198,7 +197,7 @@ impl SymbolCache { false, ); for cached_file in working_set.files() { - let path = Path::new(&*cached_file.name); + let path = std::path::Path::new(&*cached_file.name); if !path.is_file() { continue; } @@ -267,7 +266,7 @@ impl SymbolCache { } impl LanguageServer { - pub fn document_symbol( + pub(crate) fn document_symbol( &mut self, params: &DocumentSymbolParams, ) -> Option { @@ -280,7 +279,7 @@ impl LanguageServer { )) } - pub fn workspace_symbol( + pub(crate) fn workspace_symbol( &mut self, params: &WorkspaceSymbolParams, ) -> Option { @@ -298,17 +297,16 @@ impl LanguageServer { #[cfg(test)] mod tests { - use assert_json_diff::assert_json_eq; - use lsp_types::{PartialResultParams, TextDocumentIdentifier}; - use nu_test_support::fs::fixtures; - use crate::path_to_uri; use crate::tests::{initialize_language_server, open_unchecked, update}; + use assert_json_diff::assert_json_eq; use lsp_server::{Connection, Message}; use lsp_types::{ request::{DocumentSymbolRequest, Request, WorkspaceSymbolRequest}, - DocumentSymbolParams, Uri, WorkDoneProgressParams, WorkspaceSymbolParams, + DocumentSymbolParams, PartialResultParams, TextDocumentIdentifier, Uri, + WorkDoneProgressParams, WorkspaceSymbolParams, }; + use nu_test_support::fs::fixtures; fn send_document_symbol_request(client_connection: &Connection, uri: Uri) -> Message { client_connection diff --git a/crates/nu-lsp/src/workspace.rs b/crates/nu-lsp/src/workspace.rs index 3abd7ddaf4..ed6d0bba8f 100644 --- a/crates/nu-lsp/src/workspace.rs +++ b/crates/nu-lsp/src/workspace.rs @@ -1,5 +1,18 @@ +use crate::{ + ast::{find_id, find_reference_by_id}, + path_to_uri, span_to_range, uri_to_path, Id, LanguageServer, +}; use lsp_textdocument::FullTextDocument; -use nu_parser::parse; +use lsp_types::{ + DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, Location, + PrepareRenameResponse, ProgressToken, Range, ReferenceParams, RenameParams, + TextDocumentPositionParams, TextEdit, Uri, WorkspaceEdit, WorkspaceFolder, +}; +use miette::{miette, IntoDiagnostic, Result}; +use nu_protocol::{ + engine::{EngineState, StateWorkingSet}, + Span, +}; use std::{ collections::{BTreeMap, HashMap}, fs, @@ -7,72 +20,37 @@ use std::{ sync::Arc, }; -use crate::{ - ast::{find_id, find_reference_by_id}, - path_to_uri, span_to_range, uri_to_path, Id, LanguageServer, -}; -use crossbeam_channel::{Receiver, Sender}; -use lsp_server::{Message, Request, Response}; -use lsp_types::{ - DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, Location, - PrepareRenameResponse, ProgressToken, Range, ReferenceParams, RenameParams, - TextDocumentPositionParams, TextEdit, Uri, WorkspaceEdit, WorkspaceFolder, -}; -use miette::{miette, IntoDiagnostic, Result}; -use nu_glob::{glob, Paths}; -use nu_protocol::{ - engine::{EngineState, StateWorkingSet}, - Span, -}; -use serde_json::Value; - /// Message type indicating ranges of interest in each doc #[derive(Debug)] -pub struct RangePerDoc { +pub(crate) struct RangePerDoc { pub uri: Uri, pub ranges: Vec, } /// Message sent from background thread to main #[derive(Debug)] -pub enum InternalMessage { +pub(crate) enum InternalMessage { RangeMessage(RangePerDoc), Cancelled(ProgressToken), Finished(ProgressToken), OnGoing(ProgressToken, u32), } -fn find_nu_scripts_in_folder(folder_uri: &Uri) -> Result { +fn find_nu_scripts_in_folder(folder_uri: &Uri) -> Result { let path = uri_to_path(folder_uri); if !path.is_dir() { return Err(miette!("\nworkspace folder does not exist.")); } let pattern = format!("{}/**/*.nu", path.to_string_lossy()); - glob(&pattern).into_diagnostic() -} - -fn find_reference_in_file( - working_set: &mut StateWorkingSet, - file: &FullTextDocument, - fp: &Path, - id: &Id, -) -> Option> { - let block = parse( - working_set, - fp.to_str(), - file.get_content(None).as_bytes(), - false, - ); - let references: Vec = find_reference_by_id(&block, working_set, id); - - // add_block to avoid repeated parsing - working_set.add_block(block); - (!references.is_empty()).then_some(references) + nu_glob::glob(&pattern).into_diagnostic() } impl LanguageServer { /// Get initial workspace folders from initialization response - pub fn initialize_workspace_folders(&mut self, init_params: Value) -> Result<()> { + pub(crate) fn initialize_workspace_folders( + &mut self, + init_params: serde_json::Value, + ) -> Result<()> { if let Some(array) = init_params.get("workspaceFolders") { let folders: Vec = serde_json::from_value(array.clone()).into_diagnostic()?; @@ -84,7 +62,7 @@ impl LanguageServer { } /// Highlight all occurrences of the text at cursor, in current file - pub fn document_highlight( + pub(crate) fn document_highlight( &mut self, params: &DocumentHighlightParams, ) -> Option> { @@ -122,7 +100,7 @@ impl LanguageServer { /// The rename request only happens after the client received a `PrepareRenameResponse`, /// and a new name typed in, could happen before ranges ready for all files in the workspace folder - pub fn rename(&mut self, params: &RenameParams) -> Option { + pub(crate) fn rename(&mut self, params: &RenameParams) -> Option { let new_name = params.new_name.to_owned(); // changes in WorkspaceEdit have mutable key #[allow(clippy::mutable_key_type)] @@ -153,7 +131,11 @@ impl LanguageServer { /// - `timeout`: timeout in milliseconds, when timeout /// 1. Respond with all ranges found so far /// 2. Cancel the background thread - pub fn references(&mut self, params: &ReferenceParams, timeout: u128) -> Option> { + pub(crate) fn references( + &mut self, + params: &ReferenceParams, + timeout: u128, + ) -> Option> { self.occurrences = BTreeMap::new(); let mut engine_state = self.new_engine_state(); let path_uri = params.text_document_position.text_document.uri.to_owned(); @@ -213,7 +195,7 @@ impl LanguageServer { /// 1. Parse current file to find the content at the cursor that is suitable for a workspace wide renaming /// 2. Parse all nu scripts in the same workspace folder, with the variable/command name in it. /// 3. Store the results in `self.occurrences` for later rename quest - pub fn prepare_rename(&mut self, request: Request) -> Result<()> { + pub(crate) fn prepare_rename(&mut self, request: lsp_server::Request) -> Result<()> { let params: TextDocumentPositionParams = serde_json::from_value(request.params).into_diagnostic()?; self.occurrences = BTreeMap::new(); @@ -244,7 +226,7 @@ impl LanguageServer { let response = PrepareRenameResponse::Range(range); self.connection .sender - .send(Message::Response(Response { + .send(lsp_server::Message::Response(lsp_server::Response { id: request.id, result: serde_json::to_value(response).ok(), error: None, @@ -252,10 +234,7 @@ impl LanguageServer { .into_diagnostic()?; // have to clone it again in order to move to another thread - let mut engine_state = self.new_engine_state(); - engine_state - .merge_delta(working_set.render()) - .into_diagnostic()?; + let engine_state = self.new_engine_state(); let current_workspace_folder = self .get_workspace_folder_by_uri(&path_uri) .ok_or_else(|| miette!("\nCurrent file is not in any workspace"))?; @@ -273,6 +252,25 @@ impl LanguageServer { Ok(()) } + fn find_reference_in_file( + working_set: &mut StateWorkingSet, + file: &FullTextDocument, + fp: &Path, + id: &Id, + ) -> Option> { + let block = nu_parser::parse( + working_set, + fp.to_str(), + file.get_content(None).as_bytes(), + false, + ); + let references: Vec = find_reference_by_id(&block, working_set, id); + + // add_block to avoid repeated parsing + working_set.add_block(block); + (!references.is_empty()).then_some(references) + } + /// NOTE: for arguments whose declaration is in a signature /// which is not covered in the AST fn reference_not_in_ast( @@ -300,6 +298,8 @@ impl LanguageServer { None } + /// Time consuming task running in a background thread + /// communicating with the main thread using `InternalMessage` fn find_reference_in_workspace( &self, engine_state: EngineState, @@ -308,7 +308,10 @@ impl LanguageServer { span: Span, token: ProgressToken, message: String, - ) -> Result<(Sender, Arc>)> { + ) -> Result<( + crossbeam_channel::Sender, + Arc>, + )> { let (data_sender, data_receiver) = crossbeam_channel::unbounded::(); let (cancel_sender, cancel_receiver) = crossbeam_channel::bounded::(1); let engine_state = Arc::new(engine_state); @@ -371,32 +374,34 @@ impl LanguageServer { } &FullTextDocument::new("nu".to_string(), 0, content_string.into()) }; - let _ = find_reference_in_file(&mut working_set, file, fp, &id).map(|mut refs| { - let file_span = working_set - .get_span_for_filename(fp.to_string_lossy().as_ref()) - .unwrap_or(Span::unknown()); - if let Some(extra_span) = Self::reference_not_in_ast( - &id, - &working_set, - definition_span, - file_span, - span, - ) { - if !refs.contains(&extra_span) { - refs.push(extra_span) + let _ = Self::find_reference_in_file(&mut working_set, file, fp, &id).map( + |mut refs| { + let file_span = working_set + .get_span_for_filename(fp.to_string_lossy().as_ref()) + .unwrap_or(Span::unknown()); + if let Some(extra_span) = Self::reference_not_in_ast( + &id, + &working_set, + definition_span, + file_span, + span, + ) { + if !refs.contains(&extra_span) { + refs.push(extra_span) + } } - } - let ranges = refs - .iter() - .map(|span| span_to_range(span, file, file_span.start)) - .collect(); - data_sender - .send(InternalMessage::RangeMessage(RangePerDoc { uri, ranges })) - .ok(); - data_sender - .send(InternalMessage::OnGoing(token.clone(), percentage)) - .ok(); - }); + let ranges = refs + .iter() + .map(|span| span_to_range(span, file, file_span.start)) + .collect(); + data_sender + .send(InternalMessage::RangeMessage(RangePerDoc { uri, ranges })) + .ok(); + data_sender + .send(InternalMessage::OnGoing(token.clone(), percentage)) + .ok(); + }, + ); } data_sender .send(InternalMessage::Finished(token.clone())) @@ -421,21 +426,17 @@ impl LanguageServer { #[cfg(test)] mod tests { + use crate::path_to_uri; + use crate::tests::{initialize_language_server, open_unchecked, send_hover_request}; use assert_json_diff::assert_json_eq; use lsp_server::{Connection, Message}; use lsp_types::{ - request, request::Request, InitializeParams, PartialResultParams, Position, - ReferenceContext, ReferenceParams, TextDocumentIdentifier, TextDocumentPositionParams, Uri, - WorkDoneProgressParams, WorkspaceFolder, + request, request::Request, DocumentHighlightParams, InitializeParams, PartialResultParams, + Position, ReferenceContext, ReferenceParams, RenameParams, TextDocumentIdentifier, + TextDocumentPositionParams, Uri, WorkDoneProgressParams, WorkspaceFolder, }; - use lsp_types::{DocumentHighlightParams, RenameParams}; - use nu_parser::parse; - use nu_protocol::engine::StateWorkingSet; use nu_test_support::fs::fixtures; - use crate::path_to_uri; - use crate::tests::{initialize_language_server, open_unchecked, send_hover_request}; - fn send_reference_request( client_connection: &Connection, uri: Uri, @@ -873,8 +874,8 @@ mod tests { nu_protocol::Value::test_string(script_path.to_str().unwrap()), ); script_path.push("bar.nu"); - let mut working_set = StateWorkingSet::new(&engine_state); - parse( + let mut working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state); + nu_parser::parse( &mut working_set, script_path.to_str(), std::fs::read(script_path.clone()).unwrap().as_slice(),