diff --git a/crates/nu-lsp/src/diagnostics.rs b/crates/nu-lsp/src/diagnostics.rs index 1fec77d541..b6bf751fa5 100644 --- a/crates/nu-lsp/src/diagnostics.rs +++ b/crates/nu-lsp/src/diagnostics.rs @@ -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(()); }; diff --git a/crates/nu-lsp/src/hints.rs b/crates/nu-lsp/src/hints.rs new file mode 100644 index 0000000000..2fb8c373d8 --- /dev/null +++ b/crates/nu-lsp/src/hints.rs @@ -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( + ast: &Arc, + working_set: &StateWorkingSet, + extra_args: &E, + f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, +) -> Vec { + 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( + expr: &Expression, + working_set: &StateWorkingSet, + extra_args: &E, + f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, +) -> Vec { + // 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( + pattern: &MatchPattern, + working_set: &StateWorkingSet, + extra_args: &E, + f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, +) -> Vec { + 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( + redir: &PipelineRedirection, + working_set: &StateWorkingSet, + extra_args: &E, + f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, +) -> Vec { + 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> { + 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> { + Some(self.inlay_hints.get(¶ms.text_document.uri)?.clone()) + } + + pub fn extract_inlay_hints( + &self, + working_set: &StateWorkingSet, + block: &Arc, + offset: usize, + file: &FullTextDocument, + ) -> Vec { + 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: `" } + } + ]) + ); + } +} diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index be3d6fe8a1..60a214ac7a 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -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>, } pub fn path_to_uri(path: impl AsRef) -> 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, 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 { @@ -856,9 +867,9 @@ mod tests { serde_json::json!({ "uri": script, "range": { - "start": { "line": 0, "character": 4 }, - "end": { "line": 0, "character": 12 } - } + "start": { "line": 0, "character": 4 }, + "end": { "line": 0, "character": 12 } + } }) ); } @@ -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} \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 | 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} \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 | 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 { diff --git a/crates/nu-lsp/src/notification.rs b/crates/nu-lsp/src/notification.rs index 05813f83e2..281e40d72f 100644 --- a/crates/nu-lsp/src/notification.rs +++ b/crates/nu-lsp/src/notification.rs @@ -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} \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} \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" } }) ); diff --git a/tests/fixtures/lsp/hints/param.nu b/tests/fixtures/lsp/hints/param.nu new file mode 100644 index 0000000000..6d9fd7e449 --- /dev/null +++ b/tests/fixtures/lsp/hints/param.nu @@ -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)] diff --git a/tests/fixtures/lsp/hints/type.nu b/tests/fixtures/lsp/hints/type.nu new file mode 100644 index 0000000000..86e8481a5a --- /dev/null +++ b/tests/fixtures/lsp/hints/type.nu @@ -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