mirror of
https://github.com/nushell/nushell.git
synced 2025-05-30 22:57:07 +02:00
# Description Completion feature in LSP can't deal with commands defined after the cursor before this PR. This PR adds an alternative completion route where text is not truncated and no extra `a` appended. This will also ease future implementation of [signature help](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_signatureHelp). # User-Facing Changes # Tests + Formatting +6 # After Submitting
352 lines
14 KiB
Rust
352 lines
14 KiB
Rust
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, Operator, Traverse},
|
|
engine::StateWorkingSet,
|
|
Type,
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
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,
|
|
offset: &usize,
|
|
file: &FullTextDocument,
|
|
) -> Option<Vec<InlayHint>> {
|
|
let closure = |e| extract_inlay_hints_from_expression(e, working_set, offset, file);
|
|
match &expr.expr {
|
|
Expr::BinaryOp(lhs, op, rhs) => {
|
|
let mut hints: Vec<InlayHint> = [lhs, op, rhs]
|
|
.into_iter()
|
|
.flat_map(|e| e.flat_map(working_set, &closure))
|
|
.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(&expr.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_string = type_short_name(&var.ty);
|
|
Some(vec![
|
|
(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,
|
|
}),
|
|
])
|
|
}
|
|
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(expr.flat_map(working_set, &closure));
|
|
sig_idx = signatures.len();
|
|
continue;
|
|
}
|
|
// skip current for flags
|
|
Argument::Named((_, _, Some(expr))) => {
|
|
hints.extend(expr.flat_map(working_set, &closure));
|
|
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(expr.flat_map(working_set, &closure));
|
|
}
|
|
_ => {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
Some(hints)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
impl LanguageServer {
|
|
pub(crate) fn get_inlay_hints(&mut self, params: &InlayHintParams) -> Option<Vec<InlayHint>> {
|
|
self.inlay_hints.get(¶ms.text_document.uri).cloned()
|
|
}
|
|
|
|
pub(crate) fn extract_inlay_hints(
|
|
working_set: &StateWorkingSet,
|
|
block: &Arc<Block>,
|
|
offset: usize,
|
|
file: &FullTextDocument,
|
|
) -> Vec<InlayHint> {
|
|
block.flat_map(working_set, &|e| {
|
|
extract_inlay_hints_from_expression(e, working_set, &offset, file)
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::path_to_uri;
|
|
use crate::tests::{initialize_language_server, open_unchecked, result_from_message};
|
|
use assert_json_diff::assert_json_eq;
|
|
use lsp_server::{Connection, Message};
|
|
use lsp_types::{
|
|
request::{InlayHintRequest, Request},
|
|
InlayHintParams, Position, Range, TextDocumentIdentifier, Uri, WorkDoneProgressParams,
|
|
};
|
|
use nu_test_support::fs::fixtures;
|
|
|
|
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(None, None);
|
|
|
|
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());
|
|
|
|
assert_json_eq!(
|
|
result_from_message(resp),
|
|
serde_json::json!([
|
|
{ "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(None, None);
|
|
|
|
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());
|
|
|
|
assert_json_eq!(
|
|
result_from_message(resp),
|
|
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 }
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn inlay_hint_parameter_names() {
|
|
let (client_connection, _recv) = initialize_language_server(None, None);
|
|
|
|
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());
|
|
|
|
assert_json_eq!(
|
|
result_from_message(resp),
|
|
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: `" }
|
|
}
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
/// https://github.com/nushell/nushell/pull/15071
|
|
fn inlay_hint_for_nu_script_loaded_on_init() {
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("hints");
|
|
script.push("type.nu");
|
|
let script_path_str = script.to_str();
|
|
let script = path_to_uri(&script);
|
|
|
|
let config = format!("source {}", script_path_str.unwrap());
|
|
let (client_connection, _recv) = initialize_language_server(Some(&config), None);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
let resp = send_inlay_hint_request(&client_connection, script.clone());
|
|
|
|
assert_json_eq!(
|
|
result_from_message(resp),
|
|
serde_json::json!([
|
|
{ "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 }
|
|
])
|
|
);
|
|
}
|
|
}
|