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::{
ast::{
Argument, Block, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern,
PipelineRedirection, RecordItem,
Argument, Block, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Operator,
Pattern, PipelineRedirection, RecordItem,
},
engine::StateWorkingSet,
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(
expr: &Expression,
working_set: &StateWorkingSet,
@ -183,6 +193,31 @@ fn extract_inlay_hints_from_expression(
)
};
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) => {
let position = span_to_range(&span, file, *offset).end;
// skip if the type is already specified in code
@ -199,17 +234,11 @@ fn extract_inlay_hints_from_expression(
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(),
};
let type_string = type_short_name(&var.ty);
Some(vec![
(InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::String(format!(": {}", type_str_short)),
label: InlayHintLabel::String(format!(": {}", type_string)),
position,
text_edits: None,
tooltip: None,
@ -363,41 +392,47 @@ mod tests {
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
}
{ "position": { "line": 0, "character": 9 }, "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_assignment_type() {
let (client_connection, _recv) = initialize_language_server();
let mut script = fixtures();
script.push("lsp");
script.push("hints");
script.push("assignment.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": 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,
};
fn document_symbol_test(client_connection: &Connection, uri: Uri) -> Message {
fn send_document_symbol_request(client_connection: &Connection, uri: Uri) -> Message {
client_connection
.sender
.send(Message::Request(lsp_server::Request {
@ -319,7 +319,7 @@ mod tests {
.unwrap()
}
fn workspace_symbol_test(client_connection: &Connection, query: String) -> Message {
fn send_workspace_symbol_request(client_connection: &Connection, query: String) -> Message {
client_connection
.sender
.send(Message::Request(lsp_server::Request {
@ -352,7 +352,7 @@ mod tests {
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 {
response.result
} else {
@ -374,7 +374,7 @@ mod tests {
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 {
response.result
} 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 {
response.result
} else {
@ -481,7 +481,7 @@ mod tests {
open_unchecked(&client_connection, script_foo.clone());
open_unchecked(&client_connection, script_bar.clone());
let resp = workspace_symbol_test(&client_connection, "br".to_string());
let resp = send_workspace_symbol_request(&client_connection, "br".to_string());
let result = if let Message::Response(response) = resp {
response.result
} else {
@ -547,7 +547,7 @@ mod tests {
open_unchecked(&client_connection, script_foo.clone());
open_unchecked(&client_connection, script_bar.clone());
let resp = workspace_symbol_test(&client_connection, "foo".to_string());
let resp = send_workspace_symbol_request(&client_connection, "foo".to_string());
let result = if let Message::Response(response) = resp {
response.result
} 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 bool = true
let float = 1.0