feat(lsp): better completion item documentation (#14905)

# Description

This PR adds those markdown doc strings (previously only available via
hover) to completion items:

<img width="676" alt="image"
src="https://github.com/user-attachments/assets/58c44d7d-4b49-4955-b3f0-fa7a727a8bc0"
/>

It also refactors a bit, primarily to prevent namespace pollution.

# User-Facing Changes

# Tests + Formatting

# After Submitting
This commit is contained in:
zc he 2025-01-24 20:44:55 +08:00 committed by GitHub
parent fd684a204c
commit 299453ecb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 431 additions and 404 deletions

View File

@ -1,3 +1,4 @@
use crate::Id;
use nu_protocol::{ use nu_protocol::{
ast::{ ast::{
Argument, Block, Call, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern, Argument, Block, Call, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern,
@ -6,9 +7,7 @@ use nu_protocol::{
engine::StateWorkingSet, engine::StateWorkingSet,
Span, Span,
}; };
use std::{path::PathBuf, sync::Arc}; use std::sync::Arc;
use crate::Id;
/// similar to flatten_block, but allows extra map function /// similar to flatten_block, but allows extra map function
pub fn ast_flat_map<'a, T, F>( pub fn ast_flat_map<'a, T, F>(
@ -243,7 +242,7 @@ fn try_find_id_in_mod(
id_ref: Option<&Id>, id_ref: Option<&Id>,
) -> Option<(Id, Span)> { ) -> Option<(Id, Span)> {
let call_name = working_set.get_span_contents(call.head); 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; return None;
}; };
let check_location = |span: &Span| location.map_or(true, |pos| span.contains(*pos)); 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>, id: Option<&Id>,
) -> Option<(Id, Span)> { ) -> Option<(Id, Span)> {
let call_name = working_set.get_span_contents(call.head); let call_name = working_set.get_span_contents(call.head);
if call_name != "use".as_bytes() { if call_name != b"use" {
return None; return None;
} }
let find_by_name = |name: &[u8]| match id { 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 get_module_id = |span: Span| {
let span = strip_quotes(span, working_set); let span = strip_quotes(span, working_set);
let name = String::from_utf8_lossy(working_set.get_span_contents(span)); 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 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())?); let found_id = Id::Module(working_set.find_module(stem.as_bytes())?);
id.map_or(true, |id_r| found_id == *id_r) 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 /// find the leaf node at the given location from ast
pub fn find_id( pub(crate) fn find_id(
ast: &Arc<Block>, ast: &Arc<Block>,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
location: &usize, location: &usize,
@ -452,11 +451,9 @@ fn find_reference_by_id_in_expr(
.filter_map(|arg| arg.expr()) .filter_map(|arg| arg.expr())
.flat_map(recur) .flat_map(recur)
.collect(); .collect();
if let Id::Declaration(decl_id) = id { if matches!(id, Id::Declaration(decl_id) if call.decl_id == *decl_id) {
if *decl_id == call.decl_id { occurs.push(call.head);
occurs.push(call.head); return Some(occurs);
return Some(occurs);
}
} }
if let Some((_, span_found)) = try_find_id_in_def(call, working_set, None, Some(id)) 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))) .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<Block>, working_set: &StateWorkingSet, id: &Id) -> Vec<Span> { pub(crate) fn find_reference_by_id(
ast: &Arc<Block>,
working_set: &StateWorkingSet,
id: &Id,
) -> Vec<Span> {
ast_flat_map(ast, working_set, &|e| { ast_flat_map(ast, working_set, &|e| {
find_reference_by_id_in_expr(e, working_set, id) find_reference_by_id_in_expr(e, working_set, id)
}) })

View File

@ -49,11 +49,10 @@ impl LanguageServer {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use assert_json_diff::assert_json_eq;
use nu_test_support::fs::fixtures;
use crate::path_to_uri; use crate::path_to_uri;
use crate::tests::{initialize_language_server, open_unchecked, update}; use crate::tests::{initialize_language_server, open_unchecked, update};
use assert_json_diff::assert_json_eq;
use nu_test_support::fs::fixtures;
#[test] #[test]
fn publish_diagnostics_variable_does_not_exists() { fn publish_diagnostics_variable_does_not_exists() {

View File

@ -4,7 +4,10 @@ use nu_protocol::engine::StateWorkingSet;
use nu_protocol::Span; use nu_protocol::Span;
impl LanguageServer { impl LanguageServer {
pub fn find_definition_span_by_id(working_set: &StateWorkingSet, id: &Id) -> Option<Span> { pub(crate) fn find_definition_span_by_id(
working_set: &StateWorkingSet,
id: &Id,
) -> Option<Span> {
match id { match id {
Id::Declaration(decl_id) => { Id::Declaration(decl_id) => {
let block_id = working_set.get_decl(*decl_id).block_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, &mut self,
params: &GotoDefinitionParams, params: &GotoDefinitionParams,
) -> Option<GotoDefinitionResponse> { ) -> Option<GotoDefinitionResponse> {
@ -54,8 +57,8 @@ mod tests {
use crate::tests::{initialize_language_server, open_unchecked}; use crate::tests::{initialize_language_server, open_unchecked};
use assert_json_diff::assert_json_eq; use assert_json_diff::assert_json_eq;
use lsp_server::{Connection, Message}; use lsp_server::{Connection, Message};
use lsp_types::request::{GotoDefinition, Request};
use lsp_types::{ use lsp_types::{
request::{GotoDefinition, Request},
GotoDefinitionParams, PartialResultParams, Position, TextDocumentIdentifier, GotoDefinitionParams, PartialResultParams, Position, TextDocumentIdentifier,
TextDocumentPositionParams, Uri, WorkDoneProgressParams, TextDocumentPositionParams, Uri, WorkDoneProgressParams,
}; };

View File

@ -1,5 +1,3 @@
use std::sync::Arc;
use crate::ast::{ast_flat_map, expr_flat_map}; use crate::ast::{ast_flat_map, expr_flat_map};
use crate::{span_to_range, LanguageServer}; use crate::{span_to_range, LanguageServer};
use lsp_textdocument::FullTextDocument; use lsp_textdocument::FullTextDocument;
@ -12,6 +10,7 @@ use nu_protocol::{
engine::StateWorkingSet, engine::StateWorkingSet,
Type, Type,
}; };
use std::sync::Arc;
fn type_short_name(t: &Type) -> String { fn type_short_name(t: &Type) -> String {
match t { match t {
@ -145,12 +144,11 @@ fn extract_inlay_hints_from_expression(
} }
impl LanguageServer { impl LanguageServer {
pub fn get_inlay_hints(&mut self, params: &InlayHintParams) -> Option<Vec<InlayHint>> { pub(crate) fn get_inlay_hints(&mut self, params: &InlayHintParams) -> Option<Vec<InlayHint>> {
Some(self.inlay_hints.get(&params.text_document.uri)?.clone()) Some(self.inlay_hints.get(&params.text_document.uri)?.clone())
} }
pub fn extract_inlay_hints( pub(crate) fn extract_inlay_hints(
&self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
block: &Arc<Block>, block: &Arc<Block>,
offset: usize, offset: usize,
@ -164,17 +162,15 @@ impl LanguageServer {
#[cfg(test)] #[cfg(test)]
mod tests { 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::path_to_uri;
use crate::tests::{initialize_language_server, open_unchecked}; use crate::tests::{initialize_language_server, open_unchecked};
use assert_json_diff::assert_json_eq;
use lsp_server::{Connection, Message}; use lsp_server::{Connection, Message};
use lsp_types::{ use lsp_types::{
request::InlayHintRequest, InlayHintParams, Position, Range, TextDocumentIdentifier, Uri, request::{InlayHintRequest, Request},
WorkDoneProgressParams, InlayHintParams, Position, Range, TextDocumentIdentifier, Uri, WorkDoneProgressParams,
}; };
use nu_test_support::fs::fixtures;
fn send_inlay_hint_request(client_connection: &Connection, uri: Uri) -> Message { fn send_inlay_hint_request(client_connection: &Connection, uri: Uri) -> Message {
client_connection client_connection

View File

@ -1,33 +1,31 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
use ast::find_id;
use crossbeam_channel::{Receiver, Sender};
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_textdocument::{FullTextDocument, TextDocuments};
use lsp_types::{ use lsp_types::{
request::{self, Request}, request::{self, Request},
CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams,
Hover, HoverContents, HoverParams, InlayHint, Location, MarkupContent, MarkupKind, OneOf, CompletionResponse, CompletionTextEdit, Documentation, Hover, HoverContents, HoverParams,
Position, Range, ReferencesOptions, RenameOptions, ServerCapabilities, TextDocumentSyncKind, InlayHint, Location, MarkupContent, MarkupKind, OneOf, Position, Range, ReferencesOptions,
TextEdit, Uri, WorkDoneProgressOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities, RenameOptions, ServerCapabilities, TextDocumentSyncKind, TextEdit, Uri,
WorkDoneProgressOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities,
WorkspaceServerCapabilities, WorkspaceServerCapabilities,
}; };
use miette::{miette, IntoDiagnostic, Result}; use miette::{miette, IntoDiagnostic, Result};
use nu_cli::{NuCompleter, SuggestionKind}; use nu_cli::{NuCompleter, SuggestionKind};
use nu_parser::parse;
use nu_protocol::{ use nu_protocol::{
ast::Block, ast::Block,
engine::{CachedFile, EngineState, Stack, StateDelta, StateWorkingSet}, engine::{CachedFile, Command, EngineState, Stack, StateDelta, StateWorkingSet},
DeclId, ModuleId, Span, Type, Value, VarId, DeclId, ModuleId, Span, Type, VarId,
}; };
use std::{collections::BTreeMap, sync::Mutex};
use std::{ use std::{
collections::BTreeMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::Arc, sync::Arc,
sync::Mutex,
time::Duration, time::Duration,
}; };
use symbols::SymbolCache; use symbols::SymbolCache;
use url::Url;
use workspace::{InternalMessage, RangePerDoc}; use workspace::{InternalMessage, RangePerDoc};
mod ast; mod ast;
@ -39,7 +37,7 @@ mod symbols;
mod workspace; mod workspace;
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum Id { pub(crate) enum Id {
Variable(VarId), Variable(VarId),
Declaration(DeclId), Declaration(DeclId),
Value(Type), Value(Type),
@ -56,30 +54,33 @@ pub struct LanguageServer {
workspace_folders: BTreeMap<String, WorkspaceFolder>, workspace_folders: BTreeMap<String, WorkspaceFolder>,
/// for workspace wide requests /// for workspace wide requests
occurrences: BTreeMap<Uri, Vec<Range>>, occurrences: BTreeMap<Uri, Vec<Range>>,
channels: Option<(Sender<bool>, Arc<Receiver<InternalMessage>>)>, channels: Option<(
crossbeam_channel::Sender<bool>,
Arc<crossbeam_channel::Receiver<InternalMessage>>,
)>,
/// set to true when text changes /// set to true when text changes
need_parse: bool, need_parse: bool,
/// cache `StateDelta` to avoid repeated parsing /// cache `StateDelta` to avoid repeated parsing
cached_state_delta: Option<StateDelta>, cached_state_delta: Option<StateDelta>,
} }
pub fn path_to_uri(path: impl AsRef<Path>) -> Uri { pub(crate) fn path_to_uri(path: impl AsRef<Path>) -> Uri {
Uri::from_str( Uri::from_str(
Url::from_file_path(path) url::Url::from_file_path(path)
.expect("Failed to convert path to Url") .expect("Failed to convert path to Url")
.as_str(), .as_str(),
) )
.expect("Failed to convert Url to lsp_types::Uri.") .expect("Failed to convert Url to lsp_types::Uri.")
} }
pub fn uri_to_path(uri: &Uri) -> PathBuf { pub(crate) fn uri_to_path(uri: &Uri) -> PathBuf {
Url::from_str(uri.as_str()) url::Url::from_str(uri.as_str())
.expect("Failed to convert Uri to Url") .expect("Failed to convert Uri to Url")
.to_file_path() .to_file_path()
.expect("Failed to convert Url to 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 start = file.position_at(span.start.saturating_sub(offset) as u32);
let end = file.position_at(span.end.saturating_sub(offset) as u32); let end = file.position_at(span.end.saturating_sub(offset) as u32);
Range { start, end } Range { start, end }
@ -251,14 +252,14 @@ impl LanguageServer {
} }
/// Send a cancel message to a running bg thread /// 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 { if let Some((sender, _)) = &self.channels {
sender.send(true).ok(); sender.send(true).ok();
} }
} }
/// Check results from background thread /// Check results from background thread
pub fn handle_internal_messages(&mut self) -> Result<bool> { pub(crate) fn handle_internal_messages(&mut self) -> Result<bool> {
let mut reset = false; let mut reset = false;
if let Some((_, receiver)) = &self.channels { if let Some((_, receiver)) = &self.channels {
for im in receiver.try_iter() { for im in receiver.try_iter() {
@ -286,10 +287,13 @@ impl LanguageServer {
Ok(reset) 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 mut engine_state = self.initial_engine_state.clone();
let cwd = std::env::current_dir().expect("Could not get current working directory."); 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 // merge the cached `StateDelta` if text not changed
if !self.need_parse { if !self.need_parse {
engine_state engine_state
@ -303,7 +307,7 @@ impl LanguageServer {
engine_state engine_state
} }
pub fn parse_and_find<'a>( pub(crate) fn parse_and_find<'a>(
&mut self, &mut self,
engine_state: &'a mut EngineState, engine_state: &'a mut EngineState,
uri: &Uri, uri: &Uri,
@ -321,12 +325,12 @@ impl LanguageServer {
.get_document(uri) .get_document(uri)
.ok_or_else(|| miette!("\nFailed to get document"))?; .ok_or_else(|| miette!("\nFailed to get document"))?;
let location = file.offset_at(pos) as usize + file_span.start; 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_or_else(|| miette!("\nFailed to find current name"))?;
Ok((working_set, id, span, file_span.start)) Ok((working_set, id, span, file_span.start))
} }
pub fn parse_file<'a>( pub(crate) fn parse_file<'a>(
&mut self, &mut self,
engine_state: &'a mut EngineState, engine_state: &'a mut EngineState,
uri: &Uri, uri: &Uri,
@ -339,10 +343,11 @@ impl LanguageServer {
let file_path_str = file_path.to_str()?; let file_path_str = file_path.to_str()?;
let contents = file.get_content(None).as_bytes(); let contents = file.get_content(None).as_bytes();
let _ = working_set.files.push(file_path.clone(), Span::unknown()); 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)?; let span = working_set.get_span_for_filename(file_path_str)?;
if need_hints { 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); self.inlay_hints.insert(uri.clone(), file_inlay_hints);
} }
if self.need_parse { 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<Hover> { fn hover(&mut self, params: &HoverParams) -> Option<Hover> {
let mut engine_state = self.new_engine_state(); let mut engine_state = self.new_engine_state();
@ -448,150 +588,20 @@ impl LanguageServer {
match id { match id {
Id::Variable(var_id) => { Id::Variable(var_id) => {
let var = working_set.get_variable(var_id); let var = working_set.get_variable(var_id);
let contents = let contents = format!(
format!("{} `{}`", if var.mutable { "mutable " } else { "" }, var.ty); "{}{} `{}`",
if var.const_val.is_some() {
"const "
} else {
""
},
if var.mutable { "mutable " } else { "" },
var.ty,
);
markdown_hover(contents) markdown_hover(contents)
} }
Id::Declaration(decl_id) => { Id::Declaration(decl_id) => {
let decl = working_set.get_decl(decl_id); markdown_hover(Self::get_decl_description(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)
} }
Id::Module(module_id) => { Id::Module(module_id) => {
let mut description = String::new(); let mut description = String::new();
@ -612,40 +622,62 @@ impl LanguageServer {
let docs = self.docs.lock().ok()?; let docs = self.docs.lock().ok()?;
let file = docs.get_document(&path_uri)?; let file = docs.get_document(&path_uri)?;
let mut completer = NuCompleter::new( let engine_state = Arc::new(self.initial_engine_state.clone());
Arc::new(self.initial_engine_state.clone()), let mut completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new()));
Arc::new(Stack::new()),
);
let location = file.offset_at(params.text_document_position.position) as usize; let location = file.offset_at(params.text_document_position.position) as usize;
let results = completer.fetch_completions_at(&file.get_content(None)[..location], location); let results = completer.fetch_completions_at(&file.get_content(None)[..location], location);
if results.is_empty() { (!results.is_empty()).then_some(CompletionResponse::Array(
None results
} else { .into_iter()
Some(CompletionResponse::Array( .map(|r| {
results let mut start = params.text_document_position.position;
.into_iter() start.character -= (r.suggestion.span.end - r.suggestion.span.start) as u32;
.map(|r| { let decl_id = r.kind.clone().and_then(|kind| {
let mut start = params.text_document_position.position; matches!(kind, SuggestionKind::Command(_))
start.character -= (r.suggestion.span.end - r.suggestion.span.start) as u32; .then_some(engine_state.find_decl(r.suggestion.value.as_bytes(), &[])?)
});
CompletionItem { CompletionItem {
label: r.suggestion.value.clone(), label: r.suggestion.value.clone(),
detail: r.suggestion.description, label_details: r
kind: Self::lsp_completion_item_kind(r.kind), .kind
text_edit: Some(CompletionTextEdit::Edit(TextEdit { .clone()
range: Range { .map(|kind| match kind {
start, SuggestionKind::Type(t) => t.to_string(),
end: params.text_document_position.position, SuggestionKind::Command(cmd) => cmd.to_string(),
}, })
new_text: r.suggestion.value, .map(|s| CompletionItemLabelDetails {
})), detail: None,
..Default::default() description: Some(s),
} }),
}) detail: r.suggestion.description,
.collect(), 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( fn lsp_completion_item_kind(
@ -681,8 +713,9 @@ mod tests {
}; };
use nu_test_support::fs::fixtures; use nu_test_support::fs::fixtures;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
use std::time::Duration;
pub fn initialize_language_server( pub(crate) fn initialize_language_server(
params: Option<InitializeParams>, params: Option<InitializeParams>,
) -> (Connection, Receiver<Result<()>>) { ) -> (Connection, Receiver<Result<()>>) {
use std::sync::mpsc; use std::sync::mpsc;
@ -713,7 +746,7 @@ mod tests {
let _initialize_response = client_connection let _initialize_response = client_connection
.receiver .receiver
.recv_timeout(std::time::Duration::from_secs(2)) .recv_timeout(Duration::from_secs(2))
.unwrap(); .unwrap();
(client_connection, recv) (client_connection, recv)
@ -739,17 +772,17 @@ mod tests {
})) }))
.unwrap(); .unwrap();
assert!(recv assert!(recv.recv_timeout(Duration::from_secs(2)).unwrap().is_ok());
.recv_timeout(std::time::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() open(client_connection, uri).unwrap()
} }
pub fn open( pub(crate) fn open(
client_connection: &Connection, client_connection: &Connection,
uri: Uri, uri: Uri,
) -> Result<lsp_server::Notification, String> { ) -> Result<lsp_server::Notification, String> {
@ -783,7 +816,7 @@ mod tests {
} }
} }
pub fn update( pub(crate) fn update(
client_connection: &Connection, client_connection: &Connection,
uri: Uri, uri: Uri,
text: String, text: String,
@ -822,7 +855,7 @@ mod tests {
} }
} }
pub fn send_hover_request( pub(crate) fn send_hover_request(
client_connection: &Connection, client_connection: &Connection,
uri: Uri, uri: Uri,
line: u32, line: u32,
@ -846,7 +879,7 @@ mod tests {
client_connection client_connection
.receiver .receiver
.recv_timeout(std::time::Duration::from_secs(2)) .recv_timeout(Duration::from_secs(2))
.unwrap() .unwrap()
} }
@ -991,7 +1024,7 @@ mod tests {
client_connection client_connection
.receiver .receiver
.recv_timeout(std::time::Duration::from_secs(2)) .recv_timeout(Duration::from_secs(2))
.unwrap() .unwrap()
} }
@ -1014,20 +1047,18 @@ mod tests {
panic!() panic!()
}; };
assert_json_eq!( assert_json_include!(
result, actual: result,
serde_json::json!([ expected: serde_json::json!([
{ {
"label": "$greeting", "label": "$greeting",
"labelDetails": { "description": "string" },
"textEdit": { "textEdit": {
"newText": "$greeting", "newText": "$greeting",
"range": { "range": { "start": { "character": 5, "line": 2 }, "end": { "character": 9, "line": 2 } }
"start": { "character": 5, "line": 2 }, },
"end": { "character": 9, "line": 2 } "kind": 6
} }
},
"kind": 6
}
]) ])
); );
} }
@ -1051,21 +1082,17 @@ mod tests {
panic!() panic!()
}; };
assert_json_eq!( assert_json_include!(
result, actual: result,
serde_json::json!([ expected: serde_json::json!([
{ {
"label": "config nu", "label": "config nu",
"detail": "Edit nu configurations.", "detail": "Edit nu configurations.",
"textEdit": { "textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
"range": { "newText": "config nu"
"start": { "line": 0, "character": 0 }, },
"end": { "line": 0, "character": 8 }, "kind": 3
}, }
"newText": "config nu"
},
"kind": 3
}
]) ])
); );
} }
@ -1089,21 +1116,19 @@ mod tests {
panic!() panic!()
}; };
assert_json_eq!( assert_json_include!(
result, actual: result,
serde_json::json!([ expected: serde_json::json!([
{ {
"label": "str trim", "label": "str trim",
"labelDetails": { "description": "built-in" },
"detail": "Trim whitespace or specific character.", "detail": "Trim whitespace or specific character.",
"textEdit": { "textEdit": {
"range": { "range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 }, },
"start": { "line": 0, "character": 8 }, "newText": "str trim"
"end": { "line": 0, "character": 13 }, },
}, "kind": 3
"newText": "str trim" }
},
"kind": 3
}
]) ])
); );
} }
@ -1132,15 +1157,13 @@ mod tests {
expected: serde_json::json!([ expected: serde_json::json!([
{ {
"label": "overlay", "label": "overlay",
"labelDetails": { "description": "keyword" },
"textEdit": { "textEdit": {
"newText": "overlay", "newText": "overlay",
"range": { "range": { "start": { "character": 0, "line": 0 }, "end": { "character": 2, "line": 0 } }
"start": { "character": 0, "line": 0 }, },
"end": { "character": 2, "line": 0 } "kind": 14
} },
},
"kind": 14
},
]) ])
); );
} }

View File

@ -1,4 +1,4 @@
use lsp_server::{Message, RequestId, Response, ResponseError}; use crate::LanguageServer;
use lsp_types::{ use lsp_types::{
notification::{ notification::{
DidChangeTextDocument, DidChangeWorkspaceFolders, DidCloseTextDocument, DidChangeTextDocument, DidChangeWorkspaceFolders, DidCloseTextDocument,
@ -8,8 +8,6 @@ use lsp_types::{
DidOpenTextDocumentParams, ProgressParams, ProgressParamsValue, ProgressToken, Uri, DidOpenTextDocumentParams, ProgressParams, ProgressParamsValue, ProgressToken, Uri,
WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressReport, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressReport,
}; };
use crate::LanguageServer;
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
impl LanguageServer { impl LanguageServer {
@ -57,7 +55,7 @@ impl LanguageServer {
} }
} }
pub fn send_progress_notification( pub(crate) fn send_progress_notification(
&self, &self,
token: ProgressToken, token: ProgressToken,
value: WorkDoneProgress, value: WorkDoneProgress,
@ -74,7 +72,7 @@ impl LanguageServer {
.into_diagnostic() .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( self.send_progress_notification(
token, token,
WorkDoneProgress::Begin(WorkDoneProgressBegin { WorkDoneProgress::Begin(WorkDoneProgressBegin {
@ -86,7 +84,7 @@ impl LanguageServer {
) )
} }
pub fn send_progress_report( pub(crate) fn send_progress_report(
&self, &self,
token: ProgressToken, token: ProgressToken,
percentage: u32, percentage: u32,
@ -102,20 +100,29 @@ impl LanguageServer {
) )
} }
pub fn send_progress_end(&self, token: ProgressToken, message: Option<String>) -> Result<()> { pub(crate) fn send_progress_end(
&self,
token: ProgressToken,
message: Option<String>,
) -> Result<()> {
self.send_progress_notification( self.send_progress_notification(
token, token,
WorkDoneProgress::End(WorkDoneProgressEnd { message }), 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 self.connection
.sender .sender
.send(Message::Response(Response { .send(lsp_server::Message::Response(lsp_server::Response {
id, id,
result: None, result: None,
error: Some(ResponseError { error: Some(lsp_server::ResponseError {
code, code,
message, message,
data: None, data: None,
@ -128,15 +135,14 @@ impl LanguageServer {
#[cfg(test)] #[cfg(test)]
mod tests { 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::path_to_uri;
use crate::tests::{ use crate::tests::{
initialize_language_server, open, open_unchecked, send_hover_request, update, 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] #[test]
fn hover_correct_documentation_on_let() { fn hover_correct_documentation_on_let() {

View File

@ -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 crate::{path_to_uri, span_to_range, uri_to_path, Id, LanguageServer};
use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_textdocument::{FullTextDocument, TextDocuments};
use lsp_types::{ use lsp_types::{
DocumentSymbolParams, DocumentSymbolResponse, Location, Range, SymbolInformation, SymbolKind, DocumentSymbolParams, DocumentSymbolResponse, Location, Range, SymbolInformation, SymbolKind,
Uri, WorkspaceSymbolParams, WorkspaceSymbolResponse, Uri, WorkspaceSymbolParams, WorkspaceSymbolResponse,
}; };
use nu_parser::parse;
use nu_protocol::ModuleId;
use nu_protocol::{ use nu_protocol::{
engine::{CachedFile, EngineState, StateWorkingSet}, engine::{CachedFile, EngineState, StateWorkingSet},
DeclId, Span, VarId, DeclId, ModuleId, Span, VarId,
}; };
use nucleo_matcher::pattern::{AtomKind, CaseMatching, Normalization, Pattern}; use nucleo_matcher::pattern::{AtomKind, CaseMatching, Normalization, Pattern};
use nucleo_matcher::{Config, Matcher, Utf32Str}; 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 /// Struct stored in cache, uri not included
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -69,7 +68,7 @@ impl Symbol {
} }
/// Cache symbols for each opened file to avoid repeated parsing /// Cache symbols for each opened file to avoid repeated parsing
pub struct SymbolCache { pub(crate) struct SymbolCache {
/// Fuzzy matcher for symbol names /// Fuzzy matcher for symbol names
matcher: Matcher, matcher: Matcher,
/// File Uri --> Symbols /// File Uri --> Symbols
@ -187,7 +186,7 @@ impl SymbolCache {
.get_document_content(uri, None) .get_document_content(uri, None)
.expect("Failed to get_document_content!") .expect("Failed to get_document_content!")
.as_bytes(); .as_bytes();
parse( nu_parser::parse(
&mut working_set, &mut working_set,
Some( Some(
uri_to_path(uri) uri_to_path(uri)
@ -198,7 +197,7 @@ impl SymbolCache {
false, false,
); );
for cached_file in working_set.files() { 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() { if !path.is_file() {
continue; continue;
} }
@ -267,7 +266,7 @@ impl SymbolCache {
} }
impl LanguageServer { impl LanguageServer {
pub fn document_symbol( pub(crate) fn document_symbol(
&mut self, &mut self,
params: &DocumentSymbolParams, params: &DocumentSymbolParams,
) -> Option<DocumentSymbolResponse> { ) -> Option<DocumentSymbolResponse> {
@ -280,7 +279,7 @@ impl LanguageServer {
)) ))
} }
pub fn workspace_symbol( pub(crate) fn workspace_symbol(
&mut self, &mut self,
params: &WorkspaceSymbolParams, params: &WorkspaceSymbolParams,
) -> Option<WorkspaceSymbolResponse> { ) -> Option<WorkspaceSymbolResponse> {
@ -298,17 +297,16 @@ impl LanguageServer {
#[cfg(test)] #[cfg(test)]
mod tests { 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::path_to_uri;
use crate::tests::{initialize_language_server, open_unchecked, update}; use crate::tests::{initialize_language_server, open_unchecked, update};
use assert_json_diff::assert_json_eq;
use lsp_server::{Connection, Message}; use lsp_server::{Connection, Message};
use lsp_types::{ use lsp_types::{
request::{DocumentSymbolRequest, Request, WorkspaceSymbolRequest}, 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 { fn send_document_symbol_request(client_connection: &Connection, uri: Uri) -> Message {
client_connection client_connection

View File

@ -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 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::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
fs, fs,
@ -7,72 +20,37 @@ use std::{
sync::Arc, 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 /// Message type indicating ranges of interest in each doc
#[derive(Debug)] #[derive(Debug)]
pub struct RangePerDoc { pub(crate) struct RangePerDoc {
pub uri: Uri, pub uri: Uri,
pub ranges: Vec<Range>, pub ranges: Vec<Range>,
} }
/// Message sent from background thread to main /// Message sent from background thread to main
#[derive(Debug)] #[derive(Debug)]
pub enum InternalMessage { pub(crate) enum InternalMessage {
RangeMessage(RangePerDoc), RangeMessage(RangePerDoc),
Cancelled(ProgressToken), Cancelled(ProgressToken),
Finished(ProgressToken), Finished(ProgressToken),
OnGoing(ProgressToken, u32), OnGoing(ProgressToken, u32),
} }
fn find_nu_scripts_in_folder(folder_uri: &Uri) -> Result<Paths> { fn find_nu_scripts_in_folder(folder_uri: &Uri) -> Result<nu_glob::Paths> {
let path = uri_to_path(folder_uri); let path = uri_to_path(folder_uri);
if !path.is_dir() { if !path.is_dir() {
return Err(miette!("\nworkspace folder does not exist.")); return Err(miette!("\nworkspace folder does not exist."));
} }
let pattern = format!("{}/**/*.nu", path.to_string_lossy()); let pattern = format!("{}/**/*.nu", path.to_string_lossy());
glob(&pattern).into_diagnostic() nu_glob::glob(&pattern).into_diagnostic()
}
fn find_reference_in_file(
working_set: &mut StateWorkingSet,
file: &FullTextDocument,
fp: &Path,
id: &Id,
) -> Option<Vec<Span>> {
let block = parse(
working_set,
fp.to_str(),
file.get_content(None).as_bytes(),
false,
);
let references: Vec<Span> = 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)
} }
impl LanguageServer { impl LanguageServer {
/// Get initial workspace folders from initialization response /// 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") { if let Some(array) = init_params.get("workspaceFolders") {
let folders: Vec<WorkspaceFolder> = let folders: Vec<WorkspaceFolder> =
serde_json::from_value(array.clone()).into_diagnostic()?; 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 /// Highlight all occurrences of the text at cursor, in current file
pub fn document_highlight( pub(crate) fn document_highlight(
&mut self, &mut self,
params: &DocumentHighlightParams, params: &DocumentHighlightParams,
) -> Option<Vec<DocumentHighlight>> { ) -> Option<Vec<DocumentHighlight>> {
@ -122,7 +100,7 @@ impl LanguageServer {
/// The rename request only happens after the client received a `PrepareRenameResponse`, /// 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 /// 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<WorkspaceEdit> { pub(crate) fn rename(&mut self, params: &RenameParams) -> Option<WorkspaceEdit> {
let new_name = params.new_name.to_owned(); let new_name = params.new_name.to_owned();
// changes in WorkspaceEdit have mutable key // changes in WorkspaceEdit have mutable key
#[allow(clippy::mutable_key_type)] #[allow(clippy::mutable_key_type)]
@ -153,7 +131,11 @@ impl LanguageServer {
/// - `timeout`: timeout in milliseconds, when timeout /// - `timeout`: timeout in milliseconds, when timeout
/// 1. Respond with all ranges found so far /// 1. Respond with all ranges found so far
/// 2. Cancel the background thread /// 2. Cancel the background thread
pub fn references(&mut self, params: &ReferenceParams, timeout: u128) -> Option<Vec<Location>> { pub(crate) fn references(
&mut self,
params: &ReferenceParams,
timeout: u128,
) -> Option<Vec<Location>> {
self.occurrences = BTreeMap::new(); self.occurrences = BTreeMap::new();
let mut engine_state = self.new_engine_state(); let mut engine_state = self.new_engine_state();
let path_uri = params.text_document_position.text_document.uri.to_owned(); 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 /// 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. /// 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 /// 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 = let params: TextDocumentPositionParams =
serde_json::from_value(request.params).into_diagnostic()?; serde_json::from_value(request.params).into_diagnostic()?;
self.occurrences = BTreeMap::new(); self.occurrences = BTreeMap::new();
@ -244,7 +226,7 @@ impl LanguageServer {
let response = PrepareRenameResponse::Range(range); let response = PrepareRenameResponse::Range(range);
self.connection self.connection
.sender .sender
.send(Message::Response(Response { .send(lsp_server::Message::Response(lsp_server::Response {
id: request.id, id: request.id,
result: serde_json::to_value(response).ok(), result: serde_json::to_value(response).ok(),
error: None, error: None,
@ -252,10 +234,7 @@ impl LanguageServer {
.into_diagnostic()?; .into_diagnostic()?;
// have to clone it again in order to move to another thread // have to clone it again in order to move to another thread
let mut engine_state = self.new_engine_state(); let engine_state = self.new_engine_state();
engine_state
.merge_delta(working_set.render())
.into_diagnostic()?;
let current_workspace_folder = self let current_workspace_folder = self
.get_workspace_folder_by_uri(&path_uri) .get_workspace_folder_by_uri(&path_uri)
.ok_or_else(|| miette!("\nCurrent file is not in any workspace"))?; .ok_or_else(|| miette!("\nCurrent file is not in any workspace"))?;
@ -273,6 +252,25 @@ impl LanguageServer {
Ok(()) Ok(())
} }
fn find_reference_in_file(
working_set: &mut StateWorkingSet,
file: &FullTextDocument,
fp: &Path,
id: &Id,
) -> Option<Vec<Span>> {
let block = nu_parser::parse(
working_set,
fp.to_str(),
file.get_content(None).as_bytes(),
false,
);
let references: Vec<Span> = 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 /// NOTE: for arguments whose declaration is in a signature
/// which is not covered in the AST /// which is not covered in the AST
fn reference_not_in_ast( fn reference_not_in_ast(
@ -300,6 +298,8 @@ impl LanguageServer {
None None
} }
/// Time consuming task running in a background thread
/// communicating with the main thread using `InternalMessage`
fn find_reference_in_workspace( fn find_reference_in_workspace(
&self, &self,
engine_state: EngineState, engine_state: EngineState,
@ -308,7 +308,10 @@ impl LanguageServer {
span: Span, span: Span,
token: ProgressToken, token: ProgressToken,
message: String, message: String,
) -> Result<(Sender<bool>, Arc<Receiver<InternalMessage>>)> { ) -> Result<(
crossbeam_channel::Sender<bool>,
Arc<crossbeam_channel::Receiver<InternalMessage>>,
)> {
let (data_sender, data_receiver) = crossbeam_channel::unbounded::<InternalMessage>(); let (data_sender, data_receiver) = crossbeam_channel::unbounded::<InternalMessage>();
let (cancel_sender, cancel_receiver) = crossbeam_channel::bounded::<bool>(1); let (cancel_sender, cancel_receiver) = crossbeam_channel::bounded::<bool>(1);
let engine_state = Arc::new(engine_state); let engine_state = Arc::new(engine_state);
@ -371,32 +374,34 @@ impl LanguageServer {
} }
&FullTextDocument::new("nu".to_string(), 0, content_string.into()) &FullTextDocument::new("nu".to_string(), 0, content_string.into())
}; };
let _ = find_reference_in_file(&mut working_set, file, fp, &id).map(|mut refs| { let _ = Self::find_reference_in_file(&mut working_set, file, fp, &id).map(
let file_span = working_set |mut refs| {
.get_span_for_filename(fp.to_string_lossy().as_ref()) let file_span = working_set
.unwrap_or(Span::unknown()); .get_span_for_filename(fp.to_string_lossy().as_ref())
if let Some(extra_span) = Self::reference_not_in_ast( .unwrap_or(Span::unknown());
&id, if let Some(extra_span) = Self::reference_not_in_ast(
&working_set, &id,
definition_span, &working_set,
file_span, definition_span,
span, file_span,
) { span,
if !refs.contains(&extra_span) { ) {
refs.push(extra_span) if !refs.contains(&extra_span) {
refs.push(extra_span)
}
} }
} let ranges = refs
let ranges = refs .iter()
.iter() .map(|span| span_to_range(span, file, file_span.start))
.map(|span| span_to_range(span, file, file_span.start)) .collect();
.collect(); data_sender
data_sender .send(InternalMessage::RangeMessage(RangePerDoc { uri, ranges }))
.send(InternalMessage::RangeMessage(RangePerDoc { uri, ranges })) .ok();
.ok(); data_sender
data_sender .send(InternalMessage::OnGoing(token.clone(), percentage))
.send(InternalMessage::OnGoing(token.clone(), percentage)) .ok();
.ok(); },
}); );
} }
data_sender data_sender
.send(InternalMessage::Finished(token.clone())) .send(InternalMessage::Finished(token.clone()))
@ -421,21 +426,17 @@ impl LanguageServer {
#[cfg(test)] #[cfg(test)]
mod tests { 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 assert_json_diff::assert_json_eq;
use lsp_server::{Connection, Message}; use lsp_server::{Connection, Message};
use lsp_types::{ use lsp_types::{
request, request::Request, InitializeParams, PartialResultParams, Position, request, request::Request, DocumentHighlightParams, InitializeParams, PartialResultParams,
ReferenceContext, ReferenceParams, TextDocumentIdentifier, TextDocumentPositionParams, Uri, Position, ReferenceContext, ReferenceParams, RenameParams, TextDocumentIdentifier,
WorkDoneProgressParams, WorkspaceFolder, 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 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( fn send_reference_request(
client_connection: &Connection, client_connection: &Connection,
uri: Uri, uri: Uri,
@ -873,8 +874,8 @@ mod tests {
nu_protocol::Value::test_string(script_path.to_str().unwrap()), nu_protocol::Value::test_string(script_path.to_str().unwrap()),
); );
script_path.push("bar.nu"); script_path.push("bar.nu");
let mut working_set = StateWorkingSet::new(&engine_state); let mut working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
parse( nu_parser::parse(
&mut working_set, &mut working_set,
script_path.to_str(), script_path.to_str(),
std::fs::read(script_path.clone()).unwrap().as_slice(), std::fs::read(script_path.clone()).unwrap().as_slice(),