mirror of
https://github.com/nushell/nushell
synced 2025-01-12 21:29:07 +00:00
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:
parent
902e6d7a27
commit
c811d86dbd
6 changed files with 560 additions and 49 deletions
|
@ -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
472
crates/nu-lsp/src/hints.rs
Normal 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(¶ms.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: `" }
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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(¶ms.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
13
tests/fixtures/lsp/hints/param.nu
vendored
Normal 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
8
tests/fixtures/lsp/hints/type.nu
vendored
Normal 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
|
Loading…
Reference in a new issue