diff --git a/Cargo.lock b/Cargo.lock index 1ac53ad635..5047323322 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3942,6 +3942,7 @@ version = "0.101.1" dependencies = [ "assert-json-diff", "crossbeam-channel", + "fuzzy-matcher", "lsp-server", "lsp-textdocument", "lsp-types", diff --git a/crates/nu-lsp/Cargo.toml b/crates/nu-lsp/Cargo.toml index 829b1bd152..91bdebc831 100644 --- a/crates/nu-lsp/Cargo.toml +++ b/crates/nu-lsp/Cargo.toml @@ -22,6 +22,7 @@ miette = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } url = { workspace = true } +fuzzy-matcher = { workspace = true } [dev-dependencies] nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.101.1" } diff --git a/crates/nu-lsp/src/diagnostics.rs b/crates/nu-lsp/src/diagnostics.rs index 5f94f021a2..1fec77d541 100644 --- a/crates/nu-lsp/src/diagnostics.rs +++ b/crates/nu-lsp/src/diagnostics.rs @@ -1,21 +1,16 @@ -use crate::LanguageServer; +use crate::{span_to_range, LanguageServer}; use lsp_types::{ notification::{Notification, PublishDiagnostics}, Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Uri, }; use miette::{IntoDiagnostic, Result}; -use nu_protocol::Value; impl LanguageServer { 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())); + let mut engine_state = self.new_engine_state(); engine_state.generate_nu_constant(); - let Some((_, offset, working_set, file)) = - self.update_engine_state(&mut engine_state, &uri) - else { + let Some((_, offset, working_set, file)) = self.parse_file(&mut engine_state, &uri) else { return Ok(()); }; @@ -29,7 +24,7 @@ impl LanguageServer { let message = err.to_string(); diagnostics.diagnostics.push(Diagnostic { - range: Self::span_to_range(&err.span(), file, offset), + range: span_to_range(&err.span(), file, offset), severity: Some(DiagnosticSeverity::ERROR), message, ..Default::default() diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index a159447c7c..be3d6fe8a1 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -2,7 +2,10 @@ use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_types::{ - request::{Completion, GotoDefinition, HoverRequest, Request}, + request::{ + Completion, DocumentSymbolRequest, GotoDefinition, HoverRequest, Request, + WorkspaceSymbolRequest, + }, CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind, OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit, @@ -13,8 +16,8 @@ use nu_cli::{NuCompleter, SuggestionKind}; use nu_parser::{flatten_block, parse, FlatShape}; use nu_protocol::{ ast::Block, - engine::{EngineState, Stack, StateWorkingSet}, - DeclId, Span, Value, VarId, + engine::{CachedFile, EngineState, Stack, StateWorkingSet}, + DeclId, ModuleId, Span, Value, VarId, }; use std::{ path::{Path, PathBuf}, @@ -22,16 +25,19 @@ use std::{ sync::Arc, time::Duration, }; +use symbols::SymbolCache; use url::Url; mod diagnostics; mod notification; +mod symbols; #[derive(Debug)] enum Id { Variable(VarId), Declaration(DeclId), Value(FlatShape), + Module(ModuleId), } pub struct LanguageServer { @@ -39,12 +45,10 @@ pub struct LanguageServer { io_threads: Option, docs: TextDocuments, engine_state: EngineState, + symbol_cache: SymbolCache, } -pub fn path_to_uri

(path: P) -> Uri -where - P: AsRef, -{ +pub fn path_to_uri(path: impl AsRef) -> Uri { Uri::from_str( Url::from_file_path(path) .expect("Failed to convert path to Url") @@ -60,6 +64,12 @@ pub fn uri_to_path(uri: &Uri) -> PathBuf { .expect("Failed to convert Url to path") } +pub 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 } +} + impl LanguageServer { pub fn initialize_stdio_connection(engine_state: EngineState) -> Result { let (connection, io_threads) = Connection::stdio(); @@ -76,6 +86,7 @@ impl LanguageServer { io_threads, docs: TextDocuments::new(), engine_state, + symbol_cache: SymbolCache::new(), }) } @@ -87,6 +98,8 @@ impl LanguageServer { definition_provider: Some(OneOf::Left(true)), hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)), completion_provider: Some(lsp_types::CompletionOptions::default()), + document_symbol_provider: Some(OneOf::Left(true)), + workspace_symbol_provider: Some(OneOf::Left(true)), ..Default::default() }) .expect("Must be serializable"); @@ -131,6 +144,14 @@ impl LanguageServer { Completion::METHOD => { Self::handle_lsp_request(request, |params| self.complete(params)) } + DocumentSymbolRequest::METHOD => { + Self::handle_lsp_request(request, |params| self.document_symbol(params)) + } + WorkspaceSymbolRequest::METHOD => { + Self::handle_lsp_request(request, |params| { + self.workspace_symbol(params) + }) + } _ => { continue; } @@ -144,6 +165,7 @@ impl LanguageServer { Message::Response(_) => {} Message::Notification(notification) => { if let Some(updated_file) = self.handle_lsp_notification(notification) { + self.symbol_cache.mark_dirty(updated_file.clone(), true); self.publish_diagnostics_for_file(updated_file)?; } } @@ -157,7 +179,13 @@ impl LanguageServer { Ok(()) } - pub fn update_engine_state<'a>( + pub fn new_engine_state(&self) -> EngineState { + let mut engine_state = self.engine_state.clone(); + engine_state.add_env_var("PWD".into(), Value::test_string(".")); + engine_state + } + + pub fn parse_file<'a>( &mut self, engine_state: &'a mut EngineState, uri: &Uri, @@ -178,6 +206,40 @@ impl LanguageServer { Some((block, offset, working_set, file)) } + fn get_location_by_span<'a>( + &self, + files: impl Iterator, + span: &Span, + ) -> Option { + for cached_file in files.into_iter() { + 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: 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( + "nu".to_string(), + 0, + String::from_utf8((*cached_file.content).to_vec()).expect("Invalid UTF-8"), + ); + return Some(Location { + uri: target_uri, + range: span_to_range(span, &temp_doc, cached_file.covered_span.start), + }); + } + } + } + None + } + fn handle_lsp_request(req: lsp_server::Request, mut param_handler: H) -> Response where P: serde::de::DeserializeOwned, @@ -207,12 +269,6 @@ impl LanguageServer { } } - 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 find_id( flattened: Vec<(Span, FlatShape)>, location: usize, @@ -226,7 +282,7 @@ impl LanguageServer { FlatShape::Variable(var_id) | FlatShape::VarDecl(var_id) => { return Some((Id::Variable(*var_id), offset, span)); } - FlatShape::InternalCall(decl_id) => { + FlatShape::InternalCall(decl_id) | FlatShape::Custom(decl_id) => { return Some((Id::Declaration(*decl_id), offset, span)); } _ => return Some((Id::Value(shape), offset, span)), @@ -236,40 +292,8 @@ impl LanguageServer { None } - 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), - }); - } - } - } - None - } - 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 mut engine_state = self.new_engine_state(); let path_uri = params .text_document_position_params @@ -277,7 +301,7 @@ impl LanguageServer { .uri .to_owned(); let (block, file_offset, working_set, file) = - self.update_engine_state(&mut engine_state, &path_uri)?; + self.parse_file(&mut engine_state, &path_uri)?; let flattened = flatten_block(&working_set, &block); let (id, _, _) = Self::find_id( flattened, @@ -294,17 +318,19 @@ impl LanguageServer { let var = working_set.get_variable(var_id); Some(var.declaration_span) } + Id::Module(module_id) => { + let module = working_set.get_module(module_id); + module.span + } _ => None, }?; Some(GotoDefinitionResponse::Scalar( - self.get_location_by_span(&working_set, &span)?, + self.get_location_by_span(working_set.files(), &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 mut engine_state = self.new_engine_state(); let path_uri = params .text_document_position_params @@ -312,7 +338,7 @@ impl LanguageServer { .uri .to_owned(); let (block, file_offset, working_set, file) = - self.update_engine_state(&mut engine_state, &path_uri)?; + self.parse_file(&mut engine_state, &path_uri)?; let flattened = flatten_block(&working_set, &block); let (id, _, _) = Self::find_id( flattened, @@ -515,6 +541,7 @@ impl LanguageServer { range: None, }) } + _ => None, } } diff --git a/crates/nu-lsp/src/notification.rs b/crates/nu-lsp/src/notification.rs index 33ec9ed5de..05813f83e2 100644 --- a/crates/nu-lsp/src/notification.rs +++ b/crates/nu-lsp/src/notification.rs @@ -1,6 +1,10 @@ use lsp_types::{ - notification::{DidChangeTextDocument, DidOpenTextDocument, DidSaveTextDocument, Notification}, - DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, Uri, + notification::{ + DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, DidSaveTextDocument, + Notification, + }, + DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, + DidSaveTextDocumentParams, Uri, }; use crate::LanguageServer; @@ -31,6 +35,13 @@ impl LanguageServer { .expect("Expect receive DidChangeTextDocumentParams"); Some(params.text_document.uri) } + DidCloseTextDocument::METHOD => { + let params: DidCloseTextDocumentParams = + serde_json::from_value(notification.params.clone()) + .expect("Expect receive DidCloseTextDocumentParams"); + self.symbol_cache.drop(¶ms.text_document.uri); + None + } _ => None, } } diff --git a/crates/nu-lsp/src/symbols.rs b/crates/nu-lsp/src/symbols.rs new file mode 100644 index 0000000000..7cec5b5756 --- /dev/null +++ b/crates/nu-lsp/src/symbols.rs @@ -0,0 +1,566 @@ +use std::collections::{BTreeMap, HashSet}; +use std::hash::{Hash, Hasher}; + +use crate::{path_to_uri, span_to_range, uri_to_path, Id, LanguageServer}; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +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, +}; +use std::{cmp::Ordering, path::Path}; + +/// Struct stored in cache, uri not included +#[derive(Clone, Debug, Eq, PartialEq)] +struct Symbol { + name: String, + kind: SymbolKind, + range: Range, +} + +impl Hash for Symbol { + fn hash(&self, state: &mut H) { + self.range.start.hash(state); + self.range.end.hash(state); + } +} + +impl PartialOrd for Symbol { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Symbol { + fn cmp(&self, other: &Self) -> Ordering { + if self.kind == other.kind { + return self.range.start.cmp(&other.range.start); + } + match (self.kind, other.kind) { + (SymbolKind::FUNCTION, _) => Ordering::Less, + (_, SymbolKind::FUNCTION) => Ordering::Greater, + _ => self.range.start.cmp(&other.range.start), + } + } +} + +impl Symbol { + fn to_symbol_information(&self, uri: &Uri) -> SymbolInformation { + #[allow(deprecated)] + SymbolInformation { + location: Location { + uri: uri.clone(), + range: self.range, + }, + name: self.name.to_owned(), + kind: self.kind, + container_name: None, + deprecated: None, + tags: None, + } + } +} + +/// Cache symbols for each opened file to avoid repeated parsing +pub struct SymbolCache { + /// Fuzzy matcher for symbol names + matcher: SkimMatcherV2, + /// File Uri --> Symbols + cache: BTreeMap>, + /// If marked as dirty, parse on next request + dirty_flags: BTreeMap, +} + +impl SymbolCache { + pub fn new() -> Self { + SymbolCache { + matcher: SkimMatcherV2::default(), + cache: BTreeMap::new(), + dirty_flags: BTreeMap::new(), + } + } + + pub fn mark_dirty(&mut self, uri: Uri, flag: bool) { + self.dirty_flags.insert(uri, flag); + } + + fn get_symbol_by_id( + working_set: &StateWorkingSet, + id: Id, + doc: &FullTextDocument, + doc_span: &Span, + ) -> Option { + match id { + Id::Declaration(decl_id) => { + let decl = working_set.get_decl(decl_id); + let span = working_set.get_block(decl.block_id()?).span?; + // multi-doc working_set, returns None if the Id is in other files + if !doc_span.contains(span.start) { + return None; + } + Some(Symbol { + name: decl.name().to_string(), + kind: SymbolKind::FUNCTION, + range: span_to_range(&span, doc, doc_span.start), + }) + } + Id::Variable(var_id) => { + let var = working_set.get_variable(var_id); + let span = var.declaration_span; + if !doc_span.contains(span.start) || span.end == span.start { + return None; + } + let range = span_to_range(&span, doc, doc_span.start); + let name = doc.get_content(Some(range)); + // TODO: better way to filter closures with type any + if name.contains('\r') || name.contains('\n') || name.contains('{') { + return None; + } + Some(Symbol { + name: name.to_string(), + kind: SymbolKind::VARIABLE, + range, + }) + } + Id::Module(module_id) => { + let module = working_set.get_module(module_id); + let span = module.span?; + if !doc_span.contains(span.start) { + return None; + } + Some(Symbol { + name: String::from_utf8(module.name()).ok()?, + kind: SymbolKind::MODULE, + range: span_to_range(&span, doc, doc_span.start), + }) + } + _ => None, + } + } + + fn extract_all_symbols( + working_set: &StateWorkingSet, + doc: &FullTextDocument, + cached_file: &CachedFile, + ) -> Vec { + let mut all_symbols: Vec = (0..working_set.num_decls()) + .filter_map(|id| { + Self::get_symbol_by_id( + working_set, + Id::Declaration(DeclId::new(id)), + doc, + &cached_file.covered_span, + ) + }) + .chain((0..working_set.num_vars()).filter_map(|id| { + Self::get_symbol_by_id( + working_set, + Id::Variable(VarId::new(id)), + doc, + &cached_file.covered_span, + ) + })) + .chain((0..working_set.num_modules()).filter_map(|id| { + Self::get_symbol_by_id( + working_set, + Id::Module(ModuleId::new(id)), + doc, + &cached_file.covered_span, + ) + })) + // TODO: same variable symbol can be duplicated with different VarId + .collect::>() + .into_iter() + .collect(); + all_symbols.sort(); + all_symbols + } + + /// Update the symbols of given uri if marked as dirty + pub fn update(&mut self, uri: &Uri, engine_state: &EngineState, docs: &TextDocuments) { + if *self.dirty_flags.get(uri).unwrap_or(&true) { + let mut working_set = StateWorkingSet::new(engine_state); + let content = docs + .get_document_content(uri, None) + .expect("Failed to get_document_content!") + .as_bytes(); + parse( + &mut working_set, + Some( + uri_to_path(uri) + .to_str() + .expect("Failed to convert pathbuf to string"), + ), + content, + false, + ); + for cached_file in working_set.files() { + let path = Path::new(&*cached_file.name); + if !(path.exists() && path.is_file()) { + continue; + } + let target_uri = path_to_uri(path); + let new_symbols = if let Some(doc) = docs.get_document(&target_uri) { + Self::extract_all_symbols(&working_set, doc, cached_file) + } else { + let temp_doc = FullTextDocument::new( + "nu".to_string(), + 0, + String::from_utf8((*cached_file.content).to_vec()).expect("Invalid UTF-8"), + ); + Self::extract_all_symbols(&working_set, &temp_doc, cached_file) + }; + self.cache.insert(target_uri.clone(), new_symbols); + self.mark_dirty(target_uri, false); + } + self.mark_dirty(uri.clone(), false); + }; + } + + pub fn drop(&mut self, uri: &Uri) { + self.cache.remove(uri); + self.dirty_flags.remove(uri); + } + + pub fn update_all(&mut self, engine_state: &EngineState, docs: &TextDocuments) { + for uri in docs.documents().keys() { + self.update(uri, engine_state, docs); + } + } + + pub fn get_symbols_by_uri(&self, uri: &Uri) -> Option> { + Some( + self.cache + .get(uri)? + .iter() + .map(|s| s.clone().to_symbol_information(uri)) + .collect(), + ) + } + + pub fn get_fuzzy_matched_symbols(&self, query: &str) -> Vec { + self.cache + .iter() + .flat_map(|(uri, symbols)| symbols.iter().map(|s| s.clone().to_symbol_information(uri))) + .filter_map(|s| { + self.matcher.fuzzy_match(&s.name, query)?; + Some(s) + }) + .collect() + } + + pub fn any_dirty(&self) -> bool { + self.dirty_flags.values().any(|f| *f) + } +} + +impl LanguageServer { + pub fn document_symbol( + &mut self, + params: &DocumentSymbolParams, + ) -> Option { + let engine_state = self.new_engine_state(); + let uri = params.text_document.uri.to_owned(); + self.symbol_cache.update(&uri, &engine_state, &self.docs); + Some(DocumentSymbolResponse::Flat( + self.symbol_cache.get_symbols_by_uri(&uri)?, + )) + } + + pub fn workspace_symbol( + &mut self, + params: &WorkspaceSymbolParams, + ) -> Option { + if self.symbol_cache.any_dirty() { + let engine_state = self.new_engine_state(); + self.symbol_cache.update_all(&engine_state, &self.docs); + } + Some(WorkspaceSymbolResponse::Flat( + self.symbol_cache + .get_fuzzy_matched_symbols(params.query.as_str()), + )) + } +} + +#[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 lsp_server::{Connection, Message}; + use lsp_types::{ + request::{DocumentSymbolRequest, Request, WorkspaceSymbolRequest}, + DocumentSymbolParams, Uri, WorkDoneProgressParams, WorkspaceSymbolParams, + }; + + fn document_symbol_test(client_connection: &Connection, uri: Uri) -> Message { + client_connection + .sender + .send(Message::Request(lsp_server::Request { + id: 1.into(), + method: DocumentSymbolRequest::METHOD.to_string(), + params: serde_json::to_value(DocumentSymbolParams { + text_document: TextDocumentIdentifier { uri }, + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }) + .unwrap(), + })) + .unwrap(); + + client_connection + .receiver + .recv_timeout(std::time::Duration::from_secs(2)) + .unwrap() + } + + fn workspace_symbol_test(client_connection: &Connection, query: String) -> Message { + client_connection + .sender + .send(Message::Request(lsp_server::Request { + id: 2.into(), + method: WorkspaceSymbolRequest::METHOD.to_string(), + params: serde_json::to_value(WorkspaceSymbolParams { + query, + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }) + .unwrap(), + })) + .unwrap(); + client_connection + .receiver + .recv_timeout(std::time::Duration::from_secs(2)) + .unwrap() + } + + #[test] + fn document_symbol_basic() { + let (client_connection, _recv) = initialize_language_server(); + + let mut script = fixtures(); + script.push("lsp"); + script.push("symbols"); + script.push("foo.nu"); + let script = path_to_uri(&script); + + open_unchecked(&client_connection, script.clone()); + + let resp = document_symbol_test(&client_connection, script.clone()); + let result = if let Message::Response(response) = resp { + response.result + } else { + panic!() + }; + + assert_json_eq!( + result, + serde_json::json!([ + { + "name": "def_foo", + "kind": 12, + "location": { + "uri": script, + "range": { + "start": { "line": 5, "character": 15 }, + "end": { "line": 5, "character": 20 } + } + } + }, + { + "name": "var_foo", + "kind": 13, + "location": { + "uri": script, + "range": { + "start": { "line": 2, "character": 4 }, + "end": { "line": 2, "character": 11 } + } + } + } + ]) + ); + } + + #[test] + fn document_symbol_update() { + let (client_connection, _recv) = initialize_language_server(); + + let mut script = fixtures(); + script.push("lsp"); + script.push("symbols"); + script.push("bar.nu"); + let script = path_to_uri(&script); + + open_unchecked(&client_connection, script.clone()); + update( + &client_connection, + script.clone(), + String::default(), + Some(lsp_types::Range { + start: lsp_types::Position { + line: 2, + character: 0, + }, + end: lsp_types::Position { + line: 4, + character: 29, + }, + }), + ); + + let resp = document_symbol_test(&client_connection, script.clone()); + let result = if let Message::Response(response) = resp { + response.result + } else { + panic!() + }; + + assert_json_eq!( + result, + serde_json::json!([ + { + "name": "var_bar", + "kind": 13, + "location": { + "uri": script, + "range": { + "start": { "line": 0, "character": 13 }, + "end": { "line": 0, "character": 20 } + } + } + } + ]) + ); + } + + #[test] + fn workspace_symbol_current() { + let (client_connection, _recv) = initialize_language_server(); + + let mut script = fixtures(); + script.push("lsp"); + script.push("symbols"); + script.push("foo.nu"); + let script_foo = path_to_uri(&script); + + let mut script = fixtures(); + script.push("lsp"); + script.push("symbols"); + script.push("bar.nu"); + let script_bar = path_to_uri(&script); + + open_unchecked(&client_connection, script_foo.clone()); + open_unchecked(&client_connection, script_bar.clone()); + + let resp = workspace_symbol_test(&client_connection, "br".to_string()); + let result = if let Message::Response(response) = resp { + response.result + } else { + panic!() + }; + + assert_json_eq!( + result, + serde_json::json!([ + { + "name": "def_bar", + "kind": 12, + "location": { + "uri": script_bar, + "range": { + "start": { "line": 2, "character": 22 }, + "end": { "line": 2, "character": 27 } + } + } + }, + { + "name": "var_bar", + "kind": 13, + "location": { + "uri": script_bar, + "range": { + "start": { "line": 0, "character": 13 }, + "end": { "line": 0, "character": 20 } + } + } + }, + { + "name": "module_bar", + "kind": 2, + "location": { + "uri": script_bar, + "range": { + "start": { "line": 4, "character": 26 }, + "end": { "line": 4, "character": 27 } + } + } + } + ]) + ); + } + + #[test] + fn workspace_symbol_other() { + let (client_connection, _recv) = initialize_language_server(); + + let mut script = fixtures(); + script.push("lsp"); + script.push("symbols"); + script.push("foo.nu"); + let script_foo = path_to_uri(&script); + + let mut script = fixtures(); + script.push("lsp"); + script.push("symbols"); + script.push("bar.nu"); + let script_bar = path_to_uri(&script); + + open_unchecked(&client_connection, script_foo.clone()); + open_unchecked(&client_connection, script_bar.clone()); + + let resp = workspace_symbol_test(&client_connection, "foo".to_string()); + let result = if let Message::Response(response) = resp { + response.result + } else { + panic!() + }; + + assert_json_eq!( + result, + serde_json::json!([ + { + "name": "def_foo", + "kind": 12, + "location": { + "uri": script_foo, + "range": { + "start": { "line": 5, "character": 15 }, + "end": { "line": 5, "character": 20 } + } + } + }, + { + "name": "var_foo", + "kind": 13, + "location": { + "uri": script_foo, + "range": { + "start": { "line": 2, "character": 4 }, + "end": { "line": 2, "character": 11 } + } + } + } + ]) + ); + } +} diff --git a/crates/nu-protocol/src/engine/state_delta.rs b/crates/nu-protocol/src/engine/state_delta.rs index 0204dbe7f1..972c5d6e93 100644 --- a/crates/nu-protocol/src/engine/state_delta.rs +++ b/crates/nu-protocol/src/engine/state_delta.rs @@ -64,6 +64,10 @@ impl StateDelta { self.virtual_paths.len() } + pub fn num_vars(&self) -> usize { + self.vars.len() + } + pub fn num_decls(&self) -> usize { self.decls.len() } diff --git a/crates/nu-protocol/src/engine/state_working_set.rs b/crates/nu-protocol/src/engine/state_working_set.rs index f3f042ebb8..f9895735b8 100644 --- a/crates/nu-protocol/src/engine/state_working_set.rs +++ b/crates/nu-protocol/src/engine/state_working_set.rs @@ -73,6 +73,10 @@ impl<'a> StateWorkingSet<'a> { self.delta.num_virtual_paths() + self.permanent_state.num_virtual_paths() } + pub fn num_vars(&self) -> usize { + self.delta.num_vars() + self.permanent_state.num_vars() + } + pub fn num_decls(&self) -> usize { self.delta.num_decls() + self.permanent_state.num_decls() } diff --git a/tests/fixtures/lsp/symbols/bar.nu b/tests/fixtures/lsp/symbols/bar.nu new file mode 100644 index 0000000000..f86b81241e --- /dev/null +++ b/tests/fixtures/lsp/symbols/bar.nu @@ -0,0 +1,5 @@ +export const var_bar = 2 + +export def def_bar [] { 3 } + +export module module_bar { } diff --git a/tests/fixtures/lsp/symbols/foo.nu b/tests/fixtures/lsp/symbols/foo.nu new file mode 100644 index 0000000000..9fc8112ea0 --- /dev/null +++ b/tests/fixtures/lsp/symbols/foo.nu @@ -0,0 +1,6 @@ +use bar.nu [ var_bar def_bar ] + +let var_foo = 1 +def_bar + +def def_foo [] { 4 }