feat(lsp): inlay hints of types in assignments (#14809)

<!--
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 is a complementary PR of #14802

<img width="418" alt="image"
src="https://github.com/user-attachments/assets/ddf945e4-7ef0-4c73-a9fd-e68591efce03"
/>


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

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

new relative test cases

# 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-12 12:15:19 +08:00 committed by GitHub
parent f05162811c
commit 23dc1b600a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 97 additions and 53 deletions

View File

@ -8,8 +8,8 @@ use lsp_types::{
}; };
use nu_protocol::{ use nu_protocol::{
ast::{ ast::{
Argument, Block, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern, Argument, Block, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Operator,
PipelineRedirection, RecordItem, Pattern, PipelineRedirection, RecordItem,
}, },
engine::StateWorkingSet, engine::StateWorkingSet,
Type, Type,
@ -167,6 +167,16 @@ fn redirect_flat_map<T, E>(
} }
} }
fn type_short_name(t: &Type) -> String {
match t {
Type::Custom(_) => String::from("custom"),
Type::Record(_) => String::from("record"),
Type::Table(_) => String::from("table"),
Type::List(_) => String::from("list"),
_ => t.to_string(),
}
}
fn extract_inlay_hints_from_expression( fn extract_inlay_hints_from_expression(
expr: &Expression, expr: &Expression,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
@ -183,6 +193,31 @@ fn extract_inlay_hints_from_expression(
) )
}; };
match &expr.expr { match &expr.expr {
Expr::BinaryOp(lhs, op, rhs) => {
let mut hints: Vec<InlayHint> =
[lhs, op, rhs].into_iter().flat_map(|e| recur(e)).collect();
if let Expr::Operator(Operator::Assignment(_)) = op.expr {
let position = span_to_range(&lhs.span, file, *offset).end;
let type_rhs = type_short_name(&rhs.ty);
let type_lhs = type_short_name(&lhs.ty);
let type_string = match (type_lhs.as_str(), type_rhs.as_str()) {
("any", _) => type_rhs,
(_, "any") => type_lhs,
_ => type_lhs,
};
hints.push(InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::String(format!(": {}", type_string)),
position,
text_edits: None,
tooltip: None,
data: None,
padding_left: None,
padding_right: None,
})
}
Some(hints)
}
Expr::VarDecl(var_id) => { Expr::VarDecl(var_id) => {
let position = span_to_range(&span, file, *offset).end; let position = span_to_range(&span, file, *offset).end;
// skip if the type is already specified in code // skip if the type is already specified in code
@ -199,17 +234,11 @@ fn extract_inlay_hints_from_expression(
return Some(Vec::new()); return Some(Vec::new());
} }
let var = working_set.get_variable(*var_id); let var = working_set.get_variable(*var_id);
let type_str_short = match var.ty { let type_string = type_short_name(&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![ Some(vec![
(InlayHint { (InlayHint {
kind: Some(InlayHintKind::TYPE), kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::String(format!(": {}", type_str_short)), label: InlayHintLabel::String(format!(": {}", type_string)),
position, position,
text_edits: None, text_edits: None,
tooltip: None, tooltip: None,
@ -363,41 +392,47 @@ mod tests {
assert_json_eq!( assert_json_eq!(
result, result,
serde_json::json!([ serde_json::json!([
{ { "position": { "line": 0, "character": 9 }, "label": ": int", "kind": 1 },
"position": { "line": 0, "character": 7 }, { "position": { "line": 1, "character": 7 }, "label": ": string", "kind": 1 },
"label": ": int", { "position": { "line": 2, "character": 8 }, "label": ": bool", "kind": 1 },
"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": 1, "character": 7 }, { "position": { "line": 6, "character": 11 }, "label": ": closure", "kind": 1 }
"label": ": string", ])
"kind": 1 );
}, }
{
"position": { "line": 2, "character": 8 }, #[test]
"label": ": bool", fn inlay_hint_assignment_type() {
"kind": 1 let (client_connection, _recv) = initialize_language_server();
},
{ let mut script = fixtures();
"position": { "line": 3, "character": 9 }, script.push("lsp");
"label": ": float", script.push("hints");
"kind": 1 script.push("assignment.nu");
}, let script = path_to_uri(&script);
{
"position": { "line": 4, "character": 8 }, open_unchecked(&client_connection, script.clone());
"label": ": list",
"kind": 1 let resp = send_inlay_hint_request(&client_connection, script.clone());
}, let result = if let Message::Response(response) = resp {
{ response.result
"position": { "line": 5, "character": 10 }, } else {
"label": ": record", panic!()
"kind": 1 };
},
{ assert_json_eq!(
"position": { "line": 6, "character": 11 }, result,
"label": ": closure", serde_json::json!([
"kind": 1 { "position": { "line": 0, "character": 8 }, "label": ": int", "kind": 1 },
} { "position": { "line": 1, "character": 10 }, "label": ": float", "kind": 1 },
{ "position": { "line": 2, "character": 10 }, "label": ": table", "kind": 1 },
{ "position": { "line": 3, "character": 9 }, "label": ": list", "kind": 1 },
{ "position": { "line": 4, "character": 11 }, "label": ": record", "kind": 1 },
{ "position": { "line": 6, "character": 7 }, "label": ": filesize", "kind": 1 },
{ "position": { "line": 7, "character": 7 }, "label": ": filesize", "kind": 1 },
{ "position": { "line": 8, "character": 4 }, "label": ": filesize", "kind": 1 }
]) ])
); );
} }

View File

@ -298,7 +298,7 @@ mod tests {
DocumentSymbolParams, Uri, WorkDoneProgressParams, WorkspaceSymbolParams, DocumentSymbolParams, Uri, WorkDoneProgressParams, WorkspaceSymbolParams,
}; };
fn document_symbol_test(client_connection: &Connection, uri: Uri) -> Message { fn send_document_symbol_request(client_connection: &Connection, uri: Uri) -> Message {
client_connection client_connection
.sender .sender
.send(Message::Request(lsp_server::Request { .send(Message::Request(lsp_server::Request {
@ -319,7 +319,7 @@ mod tests {
.unwrap() .unwrap()
} }
fn workspace_symbol_test(client_connection: &Connection, query: String) -> Message { fn send_workspace_symbol_request(client_connection: &Connection, query: String) -> Message {
client_connection client_connection
.sender .sender
.send(Message::Request(lsp_server::Request { .send(Message::Request(lsp_server::Request {
@ -352,7 +352,7 @@ mod tests {
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = document_symbol_test(&client_connection, script.clone()); let resp = send_document_symbol_request(&client_connection, script.clone());
let result = if let Message::Response(response) = resp { let result = if let Message::Response(response) = resp {
response.result response.result
} else { } else {
@ -374,7 +374,7 @@ mod tests {
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = document_symbol_test(&client_connection, script.clone()); let resp = send_document_symbol_request(&client_connection, script.clone());
let result = if let Message::Response(response) = resp { let result = if let Message::Response(response) = resp {
response.result response.result
} else { } else {
@ -437,7 +437,7 @@ mod tests {
}), }),
); );
let resp = document_symbol_test(&client_connection, script.clone()); let resp = send_document_symbol_request(&client_connection, script.clone());
let result = if let Message::Response(response) = resp { let result = if let Message::Response(response) = resp {
response.result response.result
} else { } else {
@ -481,7 +481,7 @@ mod tests {
open_unchecked(&client_connection, script_foo.clone()); open_unchecked(&client_connection, script_foo.clone());
open_unchecked(&client_connection, script_bar.clone()); open_unchecked(&client_connection, script_bar.clone());
let resp = workspace_symbol_test(&client_connection, "br".to_string()); let resp = send_workspace_symbol_request(&client_connection, "br".to_string());
let result = if let Message::Response(response) = resp { let result = if let Message::Response(response) = resp {
response.result response.result
} else { } else {
@ -547,7 +547,7 @@ mod tests {
open_unchecked(&client_connection, script_foo.clone()); open_unchecked(&client_connection, script_foo.clone());
open_unchecked(&client_connection, script_bar.clone()); open_unchecked(&client_connection, script_bar.clone());
let resp = workspace_symbol_test(&client_connection, "foo".to_string()); let resp = send_workspace_symbol_request(&client_connection, "foo".to_string());
let result = if let Message::Response(response) = resp { let result = if let Message::Response(response) = resp {
response.result response.result
} else { } else {

View File

@ -0,0 +1,9 @@
$env.int = 1
$env.float = 1.1
$env.table = [[]; []]
$env.list ++= []
$env.record = {}
mut foo = 1kb
let bar = 1kb
$foo += $bar

View File

@ -1,4 +1,4 @@
let int = 1 const int = 1
let str = "hello" let str = "hello"
let bool = true let bool = true
let float = 1.0 let float = 1.0