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:
zc he 2025-01-09 21:19:32 +08:00 committed by GitHub
parent f360489f1e
commit 3f8dd1b705
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 685 additions and 65 deletions

1
Cargo.lock generated
View file

@ -3942,6 +3942,7 @@ version = "0.101.1"
dependencies = [
"assert-json-diff",
"crossbeam-channel",
"fuzzy-matcher",
"lsp-server",
"lsp-textdocument",
"lsp-types",

View file

@ -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" }

View file

@ -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()

View file

@ -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,
}
}

View file

@ -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(&params.text_document.uri);
None
}
_ => None,
}
}

View 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 }
}
}
}
])
);
}
}

View file

@ -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()
}

View file

@ -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
View 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
View file

@ -0,0 +1,6 @@
use bar.nu [ var_bar def_bar ]
let var_foo = 1
def_bar
def def_foo [] { 4 }