mirror of
https://github.com/nushell/nushell
synced 2025-01-26 11:55:20 +00:00
feat(lsp): document_symbols and workspace_symbols (#14770)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR adds symbols related features to lsp <img width="940" alt="image" src="https://github.com/user-attachments/assets/aeaed338-133c-430a-b966-58a9bc445211" /> Notice that symbols of type variable may got filtered by client side plugins <img width="906" alt="image" src="https://github.com/user-attachments/assets/e031b3dc-443a-486f-8a35-4415c07196d0" /> # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
This commit is contained in:
parent
f360489f1e
commit
3f8dd1b705
10 changed files with 685 additions and 65 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3942,6 +3942,7 @@ version = "0.101.1"
|
|||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"crossbeam-channel",
|
||||
"fuzzy-matcher",
|
||||
"lsp-server",
|
||||
"lsp-textdocument",
|
||||
"lsp-types",
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<IoThreads>,
|
||||
docs: TextDocuments,
|
||||
engine_state: EngineState,
|
||||
symbol_cache: SymbolCache,
|
||||
}
|
||||
|
||||
pub fn path_to_uri<P>(path: P) -> Uri
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
pub fn path_to_uri(path: impl AsRef<Path>) -> 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<Self> {
|
||||
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<Item = &'a CachedFile>,
|
||||
span: &Span,
|
||||
) -> Option<Location> {
|
||||
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<P, H, R>(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<Location> {
|
||||
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<GotoDefinitionResponse> {
|
||||
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<Hover> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
566
crates/nu-lsp/src/symbols.rs
Normal file
566
crates/nu-lsp/src/symbols.rs
Normal file
|
@ -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<H: Hasher>(&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<std::cmp::Ordering> {
|
||||
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<Uri, Vec<Symbol>>,
|
||||
/// If marked as dirty, parse on next request
|
||||
dirty_flags: BTreeMap<Uri, bool>,
|
||||
}
|
||||
|
||||
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<Symbol> {
|
||||
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<Symbol> {
|
||||
let mut all_symbols: Vec<Symbol> = (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::<HashSet<Symbol>>()
|
||||
.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<Vec<SymbolInformation>> {
|
||||
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<SymbolInformation> {
|
||||
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<DocumentSymbolResponse> {
|
||||
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<WorkspaceSymbolResponse> {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
5
tests/fixtures/lsp/symbols/bar.nu
vendored
Normal file
5
tests/fixtures/lsp/symbols/bar.nu
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const var_bar = 2
|
||||
|
||||
export def def_bar [] { 3 }
|
||||
|
||||
export module module_bar { }
|
6
tests/fixtures/lsp/symbols/foo.nu
vendored
Normal file
6
tests/fixtures/lsp/symbols/foo.nu
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
use bar.nu [ var_bar def_bar ]
|
||||
|
||||
let var_foo = 1
|
||||
def_bar
|
||||
|
||||
def def_foo [] { 4 }
|
Loading…
Reference in a new issue