feat(lsp): inlay hints of variable types and command params (#14802)

<!--
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 inlay hints of variable types and parameter names to
lsp-server
<img width="547" alt="image"
src="https://github.com/user-attachments/assets/07a0dd84-5ecc-47df-a8a7-732631715662"
/>

Some design choices I made:
* for composite types like `record<foo: <record ...>>`, only a short
name displayed. Full signature already available through `hover`
* only parameter names of user defined commands are returned, feels too
much distraction if enabled for all builtins
* some information are lost in flattened expressions, so I implemented
my AST traversing functions, which may seem unnecessary, but I can't
find alternatives from the existing code.
* another minor change: added a line separator to current hover markdown
message.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

Users who think this feature annoying now have to manually turn it off
(or config the lsp client capabilities).

# 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-11 21:13:55 +08:00 committed by GitHub
parent 902e6d7a27
commit c811d86dbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 560 additions and 49 deletions

View file

@ -10,7 +10,8 @@ impl LanguageServer {
let mut engine_state = self.new_engine_state();
engine_state.generate_nu_constant();
let Some((_, offset, working_set, file)) = self.parse_file(&mut engine_state, &uri) else {
let Some((_, offset, working_set, file)) = self.parse_file(&mut engine_state, &uri, true)
else {
return Ok(());
};

472
crates/nu-lsp/src/hints.rs Normal file
View file

@ -0,0 +1,472 @@
use std::sync::Arc;
use crate::{span_to_range, LanguageServer};
use lsp_textdocument::FullTextDocument;
use lsp_types::{
InlayHint, InlayHintKind, InlayHintLabel, InlayHintParams, InlayHintTooltip, MarkupContent,
MarkupKind, Position, Range,
};
use nu_protocol::{
ast::{
Argument, Block, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern,
PipelineRedirection, RecordItem,
},
engine::StateWorkingSet,
Type,
};
/// similar to flatten_block, but allows extra map function
fn ast_flat_map<T, E>(
ast: &Arc<Block>,
working_set: &StateWorkingSet,
extra_args: &E,
f_special: fn(&Expression, &StateWorkingSet, &E) -> Option<Vec<T>>,
) -> Vec<T> {
ast.pipelines
.iter()
.flat_map(|pipeline| {
pipeline.elements.iter().flat_map(|element| {
expr_flat_map(&element.expr, working_set, extra_args, f_special)
.into_iter()
.chain(
element
.redirection
.as_ref()
.map(|redir| {
redirect_flat_map(redir, working_set, extra_args, f_special)
})
.unwrap_or_default(),
)
})
})
.collect()
}
/// generic function that do flat_map on an expression
/// concats all recursive results on sub-expressions
///
/// # Arguments
/// * `f_special` - function that overrides the default behavior
fn expr_flat_map<T, E>(
expr: &Expression,
working_set: &StateWorkingSet,
extra_args: &E,
f_special: fn(&Expression, &StateWorkingSet, &E) -> Option<Vec<T>>,
) -> Vec<T> {
// behavior overridden by f_special
if let Some(vec) = f_special(expr, working_set, extra_args) {
return vec;
}
let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special);
match &expr.expr {
Expr::RowCondition(block_id)
| Expr::Subexpression(block_id)
| Expr::Block(block_id)
| Expr::Closure(block_id) => {
let block = working_set.get_block(block_id.to_owned());
ast_flat_map(block, working_set, extra_args, f_special)
}
Expr::Range(range) => [&range.from, &range.next, &range.to]
.iter()
.filter_map(|e| e.as_ref())
.flat_map(recur)
.collect(),
Expr::Call(call) => call
.arguments
.iter()
.filter_map(|arg| arg.expr())
.flat_map(recur)
.collect(),
Expr::ExternalCall(head, args) => recur(head)
.into_iter()
.chain(args.iter().flat_map(|arg| match arg {
ExternalArgument::Regular(e) | ExternalArgument::Spread(e) => recur(e),
}))
.collect(),
Expr::UnaryNot(expr) | Expr::Collect(_, expr) => recur(expr),
Expr::BinaryOp(lhs, op, rhs) => recur(lhs)
.into_iter()
.chain(recur(op))
.chain(recur(rhs))
.collect(),
Expr::MatchBlock(matches) => matches
.iter()
.flat_map(|(pattern, expr)| {
match_pattern_flat_map(pattern, working_set, extra_args, f_special)
.into_iter()
.chain(recur(expr))
})
.collect(),
Expr::List(items) => items
.iter()
.flat_map(|item| match item {
ListItem::Item(expr) | ListItem::Spread(_, expr) => recur(expr),
})
.collect(),
Expr::Record(items) => items
.iter()
.flat_map(|item| match item {
RecordItem::Spread(_, expr) => recur(expr),
RecordItem::Pair(key, val) => [key, val].into_iter().flat_map(recur).collect(),
})
.collect(),
Expr::Table(table) => table
.columns
.iter()
.flat_map(recur)
.chain(table.rows.iter().flat_map(|row| row.iter().flat_map(recur)))
.collect(),
Expr::ValueWithUnit(vu) => recur(&vu.expr),
Expr::FullCellPath(fcp) => recur(&fcp.head),
Expr::Keyword(kw) => recur(&kw.expr),
Expr::StringInterpolation(vec) | Expr::GlobInterpolation(vec, _) => {
vec.iter().flat_map(recur).collect()
}
_ => Vec::new(),
}
}
/// flat_map on match patterns
fn match_pattern_flat_map<T, E>(
pattern: &MatchPattern,
working_set: &StateWorkingSet,
extra_args: &E,
f_special: fn(&Expression, &StateWorkingSet, &E) -> Option<Vec<T>>,
) -> Vec<T> {
let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special);
let recur_match = |p| match_pattern_flat_map(p, working_set, extra_args, f_special);
match &pattern.pattern {
Pattern::Expression(expr) => recur(expr),
Pattern::List(patterns) | Pattern::Or(patterns) => {
patterns.iter().flat_map(recur_match).collect()
}
Pattern::Record(entries) => entries.iter().flat_map(|(_, p)| recur_match(p)).collect(),
_ => Vec::new(),
}
.into_iter()
.chain(pattern.guard.as_ref().map(|g| recur(g)).unwrap_or_default())
.collect()
}
/// flat_map on redirections
fn redirect_flat_map<T, E>(
redir: &PipelineRedirection,
working_set: &StateWorkingSet,
extra_args: &E,
f_special: fn(&Expression, &StateWorkingSet, &E) -> Option<Vec<T>>,
) -> Vec<T> {
let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special);
match redir {
PipelineRedirection::Single { target, .. } => target.expr().map(recur).unwrap_or_default(),
PipelineRedirection::Separate { out, err } => [out, err]
.iter()
.filter_map(|t| t.expr())
.flat_map(recur)
.collect(),
}
}
fn extract_inlay_hints_from_expression(
expr: &Expression,
working_set: &StateWorkingSet,
extra_args: &(usize, &FullTextDocument),
) -> Option<Vec<InlayHint>> {
let span = expr.span;
let (offset, file) = extra_args;
let recur = |expr| {
expr_flat_map(
expr,
working_set,
extra_args,
extract_inlay_hints_from_expression,
)
};
match &expr.expr {
Expr::VarDecl(var_id) => {
let position = span_to_range(&span, file, *offset).end;
// skip if the type is already specified in code
if file
.get_content(Some(Range {
start: position,
end: Position {
line: position.line,
character: position.character + 1,
},
}))
.contains(':')
{
return Some(Vec::new());
}
let var = working_set.get_variable(*var_id);
let type_str_short = match var.ty {
Type::Custom(_) => String::from("custom"),
Type::Record(_) => String::from("record"),
Type::Table(_) => String::from("table"),
Type::List(_) => String::from("list"),
_ => var.ty.to_string(),
};
Some(vec![
(InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::String(format!(": {}", type_str_short)),
position,
text_edits: None,
tooltip: None,
data: None,
padding_left: None,
padding_right: None,
}),
])
}
Expr::Call(call) => {
let decl = working_set.get_decl(call.decl_id);
// skip those defined outside of the project
working_set.get_block(decl.block_id()?).span?;
let signatures = decl.signature();
let signatures = [
signatures.required_positional,
signatures.optional_positional,
]
.concat();
let arguments = &call.arguments;
let mut sig_idx = 0;
let mut hints = Vec::new();
for arg in arguments {
match arg {
// skip the rest when spread/unknown arguments encountered
Argument::Spread(expr) | Argument::Unknown(expr) => {
hints.extend(recur(expr));
sig_idx = signatures.len();
continue;
}
// skip current for flags
Argument::Named((_, _, Some(expr))) => {
hints.extend(recur(expr));
continue;
}
Argument::Positional(expr) => {
if let Some(sig) = signatures.get(sig_idx) {
sig_idx += 1;
let position = span_to_range(&arg.span(), file, *offset).start;
hints.push(InlayHint {
kind: Some(InlayHintKind::PARAMETER),
label: InlayHintLabel::String(format!("{}:", sig.name)),
position,
text_edits: None,
tooltip: Some(InlayHintTooltip::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("`{}: {}`", sig.shape, sig.desc),
})),
data: None,
padding_left: None,
padding_right: None,
});
}
hints.extend(recur(expr));
}
_ => {
continue;
}
}
}
Some(hints)
}
_ => None,
}
}
impl LanguageServer {
pub fn get_inlay_hints(&mut self, params: &InlayHintParams) -> Option<Vec<InlayHint>> {
Some(self.inlay_hints.get(&params.text_document.uri)?.clone())
}
pub fn extract_inlay_hints(
&self,
working_set: &StateWorkingSet,
block: &Arc<Block>,
offset: usize,
file: &FullTextDocument,
) -> Vec<InlayHint> {
ast_flat_map(
block,
working_set,
&(offset, file),
extract_inlay_hints_from_expression,
)
}
}
#[cfg(test)]
mod tests {
use assert_json_diff::assert_json_eq;
use lsp_types::request::Request;
use nu_test_support::fs::fixtures;
use crate::path_to_uri;
use crate::tests::{initialize_language_server, open_unchecked};
use lsp_server::{Connection, Message};
use lsp_types::{
request::InlayHintRequest, InlayHintParams, Position, Range, TextDocumentIdentifier, Uri,
WorkDoneProgressParams,
};
fn send_inlay_hint_request(client_connection: &Connection, uri: Uri) -> Message {
client_connection
.sender
.send(Message::Request(lsp_server::Request {
id: 1.into(),
method: InlayHintRequest::METHOD.to_string(),
params: serde_json::to_value(InlayHintParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
// all inlay hints in the file are returned anyway
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
},
})
.unwrap(),
}))
.unwrap();
client_connection
.receiver
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap()
}
#[test]
fn inlay_hint_variable_type() {
let (client_connection, _recv) = initialize_language_server();
let mut script = fixtures();
script.push("lsp");
script.push("hints");
script.push("type.nu");
let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone());
let resp = send_inlay_hint_request(&client_connection, script.clone());
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!([
{
"position": { "line": 0, "character": 7 },
"label": ": int",
"kind": 1
},
{
"position": { "line": 1, "character": 7 },
"label": ": string",
"kind": 1
},
{
"position": { "line": 2, "character": 8 },
"label": ": bool",
"kind": 1
},
{
"position": { "line": 3, "character": 9 },
"label": ": float",
"kind": 1
},
{
"position": { "line": 4, "character": 8 },
"label": ": list",
"kind": 1
},
{
"position": { "line": 5, "character": 10 },
"label": ": record",
"kind": 1
},
{
"position": { "line": 6, "character": 11 },
"label": ": closure",
"kind": 1
}
])
);
}
#[test]
fn inlay_hint_parameter_names() {
let (client_connection, _recv) = initialize_language_server();
let mut script = fixtures();
script.push("lsp");
script.push("hints");
script.push("param.nu");
let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone());
let resp = send_inlay_hint_request(&client_connection, script.clone());
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!([
{
"position": { "line": 9, "character": 9 },
"label": "a1:",
"kind": 2,
"tooltip": { "kind": "markdown", "value": "`any: `" }
},
{
"position": { "line": 9, "character": 11 },
"label": "a2:",
"kind": 2,
"tooltip": { "kind": "markdown", "value": "`any: `" }
},
{
"position": { "line": 9, "character": 18 },
"label": "a3:",
"kind": 2,
"tooltip": { "kind": "markdown", "value": "`any: arg3`" }
},
{
"position": { "line": 10, "character": 6 },
"label": "a1:",
"kind": 2,
"tooltip": { "kind": "markdown", "value": "`any: `" }
},
{
"position": { "line": 11, "character": 2 },
"label": "a2:",
"kind": 2,
"tooltip": { "kind": "markdown", "value": "`any: `" }
},
{
"position": { "line": 12, "character": 11 },
"label": "a1:",
"kind": 2,
"tooltip": { "kind": "markdown", "value": "`any: `" }
},
{
"position": { "line": 12, "character": 13 },
"label": "a2:",
"kind": 2,
"tooltip": { "kind": "markdown", "value": "`any: `" }
}
])
);
}
}

View file

@ -3,13 +3,13 @@ use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
use lsp_textdocument::{FullTextDocument, TextDocuments};
use lsp_types::{
request::{
Completion, DocumentSymbolRequest, GotoDefinition, HoverRequest, Request,
Completion, DocumentSymbolRequest, GotoDefinition, HoverRequest, InlayHintRequest, Request,
WorkspaceSymbolRequest,
},
CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location,
MarkupContent, MarkupKind, OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit,
Uri,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, InlayHint,
Location, MarkupContent, MarkupKind, OneOf, Range, ServerCapabilities, TextDocumentSyncKind,
TextEdit, Uri,
};
use miette::{IntoDiagnostic, Result};
use nu_cli::{NuCompleter, SuggestionKind};
@ -19,6 +19,7 @@ use nu_protocol::{
engine::{CachedFile, EngineState, Stack, StateWorkingSet},
DeclId, ModuleId, Span, Value, VarId,
};
use std::collections::BTreeMap;
use std::{
path::{Path, PathBuf},
str::FromStr,
@ -29,6 +30,7 @@ use symbols::SymbolCache;
use url::Url;
mod diagnostics;
mod hints;
mod notification;
mod symbols;
@ -46,6 +48,7 @@ pub struct LanguageServer {
docs: TextDocuments,
engine_state: EngineState,
symbol_cache: SymbolCache,
inlay_hints: BTreeMap<Uri, Vec<InlayHint>>,
}
pub fn path_to_uri(path: impl AsRef<Path>) -> Uri {
@ -87,6 +90,7 @@ impl LanguageServer {
docs: TextDocuments::new(),
engine_state,
symbol_cache: SymbolCache::new(),
inlay_hints: BTreeMap::new(),
})
}
@ -100,6 +104,7 @@ impl LanguageServer {
completion_provider: Some(lsp_types::CompletionOptions::default()),
document_symbol_provider: Some(OneOf::Left(true)),
workspace_symbol_provider: Some(OneOf::Left(true)),
inlay_hint_provider: Some(OneOf::Left(true)),
..Default::default()
})
.expect("Must be serializable");
@ -152,6 +157,9 @@ impl LanguageServer {
self.workspace_symbol(params)
})
}
InlayHintRequest::METHOD => {
Self::handle_lsp_request(request, |params| self.get_inlay_hints(params))
}
_ => {
continue;
}
@ -189,6 +197,7 @@ impl LanguageServer {
&mut self,
engine_state: &'a mut EngineState,
uri: &Uri,
need_hints: bool,
) -> Option<(Arc<Block>, usize, StateWorkingSet<'a>, &FullTextDocument)> {
let mut working_set = StateWorkingSet::new(engine_state);
let file = self.docs.get_document(uri)?;
@ -197,12 +206,14 @@ impl LanguageServer {
let contents = file.get_content(None).as_bytes();
let _ = working_set.files.push(file_path.clone(), Span::unknown());
let block = parse(&mut working_set, Some(file_path_str), contents, false);
let offset = working_set
.get_span_for_filename(file_path_str)
.unwrap_or_else(|| panic!("Failed at get_span_for_filename {}", file_path_str))
.start;
let offset = working_set.get_span_for_filename(file_path_str)?.start;
// TODO: merge delta back to engine_state?
// self.engine_state.merge_delta(working_set.render());
if need_hints {
let file_inlay_hints = self.extract_inlay_hints(&working_set, &block, offset, file);
self.inlay_hints.insert(uri.clone(), file_inlay_hints);
}
Some((block, offset, working_set, file))
}
@ -301,7 +312,7 @@ impl LanguageServer {
.uri
.to_owned();
let (block, file_offset, working_set, file) =
self.parse_file(&mut engine_state, &path_uri)?;
self.parse_file(&mut engine_state, &path_uri, false)?;
let flattened = flatten_block(&working_set, &block);
let (id, _, _) = Self::find_id(
flattened,
@ -338,7 +349,7 @@ impl LanguageServer {
.uri
.to_owned();
let (block, file_offset, working_set, file) =
self.parse_file(&mut engine_state, &path_uri)?;
self.parse_file(&mut engine_state, &path_uri, false)?;
let flattened = flatten_block(&working_set, &block);
let (id, _, _) = Self::find_id(
flattened,
@ -371,7 +382,7 @@ impl LanguageServer {
}
// Usage
description.push_str("### Usage \n```nu\n");
description.push_str("-----\n### Usage \n```nu\n");
let signature = decl.signature();
description.push_str(&format!(" {}", signature.name));
if !signature.named.is_empty() {
@ -803,7 +814,7 @@ mod tests {
}
}
fn goto_definition(
fn send_goto_definition_request(
client_connection: &Connection,
uri: Uri,
line: u32,
@ -844,7 +855,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = goto_definition(&client_connection, script.clone(), 2, 12);
let resp = send_goto_definition_request(&client_connection, script.clone(), 2, 12);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -875,7 +886,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = goto_definition(&client_connection, script.clone(), 4, 1);
let resp = send_goto_definition_request(&client_connection, script.clone(), 4, 1);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -906,7 +917,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = goto_definition(&client_connection, script.clone(), 4, 2);
let resp = send_goto_definition_request(&client_connection, script.clone(), 4, 2);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -937,7 +948,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = goto_definition(&client_connection, script.clone(), 1, 14);
let resp = send_goto_definition_request(&client_connection, script.clone(), 1, 14);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -956,7 +967,12 @@ mod tests {
);
}
pub fn hover(client_connection: &Connection, uri: Uri, line: u32, character: u32) -> Message {
pub fn send_hover_request(
client_connection: &Connection,
uri: Uri,
line: u32,
character: u32,
) -> Message {
client_connection
.sender
.send(Message::Request(lsp_server::Request {
@ -991,7 +1007,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = hover(&client_connection, script.clone(), 2, 0);
let resp = send_hover_request(&client_connection, script.clone(), 2, 0);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -1018,7 +1034,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = hover(&client_connection, script.clone(), 3, 0);
let resp = send_hover_request(&client_connection, script.clone(), 3, 0);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -1030,7 +1046,7 @@ mod tests {
serde_json::json!({
"contents": {
"kind": "markdown",
"value": "Renders some greeting message\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
"value": "Renders some greeting message\n-----\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
}
})
);
@ -1048,7 +1064,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = hover(&client_connection, script.clone(), 5, 8);
let resp = send_hover_request(&client_connection, script.clone(), 5, 8);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -1060,13 +1076,18 @@ mod tests {
serde_json::json!({
"contents": {
"kind": "markdown",
"value": "Concatenate multiple strings into a single string, with an optional separator between each.\n### Usage \n```nu\n str join {flags} <separator?>\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `separator: string` - Optional separator to use when creating string.\n\n\n### Input/output types\n\n```nu\n list<any> | string\n string | string\n\n```\n### Example(s)\n Create a string from input\n```nu\n ['nu', 'shell'] | str join\n```\n Create a string from input with a separator\n```nu\n ['nu', 'shell'] | str join '-'\n```\n"
"value": "Concatenate multiple strings into a single string, with an optional separator between each.\n-----\n### Usage \n```nu\n str join {flags} <separator?>\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `separator: string` - Optional separator to use when creating string.\n\n\n### Input/output types\n\n```nu\n list<any> | string\n string | string\n\n```\n### Example(s)\n Create a string from input\n```nu\n ['nu', 'shell'] | str join\n```\n Create a string from input with a separator\n```nu\n ['nu', 'shell'] | str join '-'\n```\n"
}
})
);
}
fn complete(client_connection: &Connection, uri: Uri, line: u32, character: u32) -> Message {
fn send_complete_request(
client_connection: &Connection,
uri: Uri,
line: u32,
character: u32,
) -> Message {
client_connection
.sender
.send(Message::Request(lsp_server::Request {
@ -1103,7 +1124,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = complete(&client_connection, script, 2, 9);
let resp = send_complete_request(&client_connection, script, 2, 9);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -1140,7 +1161,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = complete(&client_connection, script, 0, 8);
let resp = send_complete_request(&client_connection, script, 0, 8);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -1178,7 +1199,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = complete(&client_connection, script, 0, 13);
let resp = send_complete_request(&client_connection, script, 0, 13);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -1216,7 +1237,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = complete(&client_connection, script, 0, 2);
let resp = send_complete_request(&client_connection, script, 0, 2);
let result = if let Message::Response(response) = resp {
response.result
} else {

View file

@ -1,10 +1,8 @@
use lsp_types::{
notification::{
DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, DidSaveTextDocument,
Notification,
DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification,
},
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DidSaveTextDocumentParams, Uri,
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, Uri,
};
use crate::LanguageServer;
@ -23,12 +21,6 @@ impl LanguageServer {
.expect("Expect receive DidOpenTextDocumentParams");
Some(params.text_document.uri)
}
DidSaveTextDocument::METHOD => {
let params: DidSaveTextDocumentParams =
serde_json::from_value(notification.params.clone())
.expect("Expect receive DidSaveTextDocumentParams");
Some(params.text_document.uri)
}
DidChangeTextDocument::METHOD => {
let params: DidChangeTextDocumentParams =
serde_json::from_value(notification.params.clone())
@ -39,7 +31,9 @@ impl LanguageServer {
let params: DidCloseTextDocumentParams =
serde_json::from_value(notification.params.clone())
.expect("Expect receive DidCloseTextDocumentParams");
self.symbol_cache.drop(&params.text_document.uri);
let uri = params.text_document.uri;
self.symbol_cache.drop(&uri);
self.inlay_hints.remove(&uri);
None
}
_ => None,
@ -55,7 +49,9 @@ mod tests {
use nu_test_support::fs::fixtures;
use crate::path_to_uri;
use crate::tests::{hover, initialize_language_server, open, open_unchecked, update};
use crate::tests::{
initialize_language_server, open, open_unchecked, send_hover_request, update,
};
#[test]
fn hover_correct_documentation_on_let() {
@ -69,7 +65,7 @@ mod tests {
open_unchecked(&client_connection, script.clone());
let resp = hover(&client_connection, script.clone(), 0, 0);
let resp = send_hover_request(&client_connection, script.clone(), 0, 0);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -81,7 +77,7 @@ mod tests {
serde_json::json!({
"contents": {
"kind": "markdown",
"value": "Create a variable and give it a value.\n\nThis command is a parser keyword. For details, check:\n https://www.nushell.sh/book/thinking_in_nu.html\n### Usage \n```nu\n let {flags} <var_name> <initial_value>\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `var_name: any` - Variable name.\n\n `initial_value: any` - Equals sign followed by value.\n\n\n### Input/output types\n\n```nu\n any | nothing\n\n```\n### Example(s)\n Set a variable to a value\n```nu\n let x = 10\n```\n Set a variable to the result of an expression\n```nu\n let x = 10 + 100\n```\n Set a variable based on the condition\n```nu\n let x = if false { -1 } else { 1 }\n```\n"
"value": "Create a variable and give it a value.\n\nThis command is a parser keyword. For details, check:\n https://www.nushell.sh/book/thinking_in_nu.html\n-----\n### Usage \n```nu\n let {flags} <var_name> <initial_value>\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `var_name: any` - Variable name.\n\n `initial_value: any` - Equals sign followed by value.\n\n\n### Input/output types\n\n```nu\n any | nothing\n\n```\n### Example(s)\n Set a variable to a value\n```nu\n let x = 10\n```\n Set a variable to the result of an expression\n```nu\n let x = 10 + 100\n```\n Set a variable based on the condition\n```nu\n let x = if false { -1 } else { 1 }\n```\n"
}
})
);
@ -110,7 +106,7 @@ hello"#,
None,
);
let resp = hover(&client_connection, script.clone(), 3, 0);
let resp = send_hover_request(&client_connection, script.clone(), 3, 0);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -122,7 +118,7 @@ hello"#,
serde_json::json!({
"contents": {
"kind": "markdown",
"value": "Renders some updated greeting message\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
"value": "Renders some updated greeting message\n-----\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
}
})
);
@ -155,7 +151,7 @@ hello"#,
}),
);
let resp = hover(&client_connection, script.clone(), 3, 0);
let resp = send_hover_request(&client_connection, script.clone(), 3, 0);
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -167,7 +163,7 @@ hello"#,
serde_json::json!({
"contents": {
"kind": "markdown",
"value": "Renders some updated greeting message\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
"value": "Renders some updated greeting message\n-----\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
}
})
);

13
tests/fixtures/lsp/hints/param.nu vendored Normal file
View file

@ -0,0 +1,13 @@
def cmd [
a1
a2
--flag (-f)
a3? # arg3
a4?
...arg_rest
] { }
ls | cmd 1 $nu -f (
cmd 1
2
) ...[(cmd 1 2)]

8
tests/fixtures/lsp/hints/type.nu vendored Normal file
View file

@ -0,0 +1,8 @@
let int = 1
let str = "hello"
let bool = true
let float = 1.0
let list = [1 2 3]
let record = {}
let closure = {|x| $x }
let ignored: int = 5