use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; use nu_protocol::{engine::Command, PositionalArg}; use crate::{ signature::{display_flag, doc_for_arg, get_signature_label}, Id, LanguageServer, }; impl LanguageServer { pub(crate) fn get_decl_description(decl: &dyn Command, skip_description: bool) -> String { let mut description = String::new(); if !skip_description { // First description description.push_str(&format!("{}\n", decl.description().replace('\r', ""))); // Additional description if !decl.extra_description().is_empty() { description.push_str(&format!("\n{}\n", decl.extra_description())); } } // Usage description.push_str("---\n### Usage \n```nu\n"); let signature = decl.signature(); description.push_str(&get_signature_label(&signature, true)); description.push_str("\n```\n"); // Flags if !signature.named.is_empty() { description.push_str("\n### Flags\n\n"); let mut first = true; for named in signature.named { if first { first = false; } else { description.push('\n'); } description.push_str(" "); description.push_str(&display_flag(&named, true)); description.push_str(&doc_for_arg( named.arg, named.desc, named.default_value, false, )); description.push('\n'); } description.push('\n'); } // Parameters if !signature.required_positional.is_empty() || !signature.optional_positional.is_empty() || signature.rest_positional.is_some() { description.push_str("\n### Parameters\n\n"); let mut first = true; let mut write_arg = |arg: PositionalArg, optional: bool| { if first { first = false; } else { description.push('\n'); } description.push_str(&format!(" `{}`", arg.name)); description.push_str(&doc_for_arg( Some(arg.shape), arg.desc, arg.default_value, optional, )); description.push('\n'); }; for required_arg in signature.required_positional { write_arg(required_arg, false); } for optional_arg in signature.optional_positional { write_arg(optional_arg, true); } if let Some(arg) = signature.rest_positional { if !first { description.push('\n'); } description.push_str(&format!(" `...{}`", arg.name)); description.push_str(&doc_for_arg( Some(arg.shape), arg.desc, arg.default_value, false, )); description.push('\n'); } description.push('\n'); } // Input/output types if !signature.input_output_types.is_empty() { description.push_str("\n### Input/output types\n"); description.push_str("\n```nu\n"); for input_output in &signature.input_output_types { description.push_str(&format!(" {} | {}\n", input_output.0, input_output.1)); } description.push_str("\n```\n"); } // Examples if !decl.examples().is_empty() { description.push_str("### Example(s)\n"); for example in decl.examples() { description.push_str(&format!( " {}\n```nu\n {}\n```\n", example.description, example.example )); } } description } pub(crate) fn hover(&mut self, params: &HoverParams) -> Option { let path_uri = params .text_document_position_params .text_document .uri .to_owned(); let mut engine_state = self.new_engine_state(Some(&path_uri)); let (working_set, id, _, _) = self .parse_and_find( &mut engine_state, &path_uri, params.text_document_position_params.position, ) .ok()?; let markdown_hover = |content: String| { Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: content, }), // TODO range: None, }) }; match id { Id::Variable(var_id, _) => { let var = working_set.get_variable(var_id); let value = var .const_val .clone() .and_then(|v| v.coerce_into_string().ok()) .unwrap_or(String::from(if var.mutable { "mutable" } else { "immutable" })); let contents = format!("```\n{}\n``` \n---\n{}", var.ty, value); markdown_hover(contents) } Id::CellPath(var_id, cell_path) => { let var = working_set.get_variable(var_id); markdown_hover( var.const_val .as_ref() .and_then(|val| val.follow_cell_path(&cell_path).ok()) .map(|val| { let ty = val.get_type(); if let Ok(s) = val.coerce_str() { format!("```\n{}\n```\n---\n{}", ty, s) } else { format!("```\n{}\n```", ty) } }) .unwrap_or("`unknown`".into()), ) } Id::Declaration(decl_id) => markdown_hover(Self::get_decl_description( working_set.get_decl(decl_id), false, )), Id::Module(module_id, _) => { let description = working_set .get_module_comments(module_id)? .iter() .map(|sp| String::from_utf8_lossy(working_set.get_span_contents(*sp)).into()) .collect::>() .join("\n"); markdown_hover(description) } Id::Value(t) => markdown_hover(format!("`{}`", t)), Id::External(cmd) => { let command_output = if cfg!(windows) { std::process::Command::new("powershell.exe") .args(["-NoProfile", "-Command", "help", &cmd]) .output() } else { std::process::Command::new("man").arg(&cmd).output() }; let manpage_str = match command_output { Ok(output) => nu_utils::strip_ansi_likely( String::from_utf8_lossy(&output.stdout).as_ref(), ) .to_string(), Err(_) => format!("No command help found for {}", &cmd), }; markdown_hover(manpage_str) } } } } #[cfg(test)] mod hover_tests { use crate::{ path_to_uri, tests::{ initialize_language_server, open_unchecked, result_from_message, send_hover_request, }, }; use assert_json_diff::assert_json_eq; use nu_test_support::fs::fixtures; #[test] fn hover_on_variable() { let (client_connection, _recv) = initialize_language_server(None, None); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("var.nu"); let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); let resp = send_hover_request(&client_connection, script, 2, 0); assert_json_eq!( result_from_message(resp), serde_json::json!({ "contents": { "kind": "markdown", "value": "```\ntable\n``` \n---\nimmutable" } }) ); } #[test] fn hover_on_cell_path() { let (client_connection, _recv) = initialize_language_server(None, None); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("use.nu"); let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); let resp = send_hover_request(&client_connection, script.clone(), 2, 3); let result = result_from_message(resp); assert_json_eq!( result.pointer("/contents/value").unwrap(), serde_json::json!("```\nlist\n```") ); let resp = send_hover_request(&client_connection, script.clone(), 2, 7); let result = result_from_message(resp); assert_json_eq!( result.pointer("/contents/value").unwrap(), serde_json::json!("```\nrecord\n```") ); let resp = send_hover_request(&client_connection, script, 2, 11); let result = result_from_message(resp); assert_json_eq!( result.pointer("/contents/value").unwrap(), serde_json::json!("```\nint\n```\n---\n2") ); let mut script = fixtures(); script.push("lsp"); script.push("workspace"); script.push("baz.nu"); let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); // For module record let resp = send_hover_request(&client_connection, script, 8, 42); let result = result_from_message(resp); assert_json_eq!( result.pointer("/contents/value").unwrap(), serde_json::json!("```\nstring\n```\n---\nconst value") ); } #[test] fn hover_on_custom_command() { let (client_connection, _recv) = initialize_language_server(None, None); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("command.nu"); let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); let resp = send_hover_request(&client_connection, script, 3, 0); assert_json_eq!( result_from_message(resp), serde_json::json!({ "contents": { "kind": "markdown", "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" } }) ); } #[test] fn hover_on_custom_in_custom() { let (client_connection, _recv) = initialize_language_server(None, None); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("command.nu"); let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); let resp = send_hover_request(&client_connection, script, 9, 7); assert_json_eq!( result_from_message(resp), serde_json::json!({ "contents": { "kind": "markdown", "value": "\n---\n### Usage \n```nu\n bar {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n" } }) ); } #[test] fn hover_on_external_command() { let (client_connection, _recv) = initialize_language_server(None, None); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("command.nu"); let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); let resp = send_hover_request(&client_connection, script, 6, 2); let hover_text = result_from_message(resp) .pointer("/contents/value") .unwrap() .as_str() .unwrap() .to_string(); #[cfg(not(windows))] assert!(hover_text.contains("SLEEP")); #[cfg(windows)] assert!(hover_text.contains("Start-Sleep")); } #[test] fn hover_on_str_join() { let (client_connection, _recv) = initialize_language_server(None, None); let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("command.nu"); let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); let resp = send_hover_request(&client_connection, script, 5, 8); assert_json_eq!( result_from_message(resp), serde_json::json!({ "contents": { "kind": "markdown", "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`: `` - Optional separator to use when creating string. (optional)\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" } }) ); } #[test] fn hover_on_module() { let (client_connection, _recv) = initialize_language_server(None, None); let mut script = fixtures(); script.push("lsp"); script.push("workspace"); script.push("foo.nu"); let script = path_to_uri(&script); open_unchecked(&client_connection, script.clone()); let resp = send_hover_request(&client_connection, script.clone(), 15, 15); let result = result_from_message(resp); assert_eq!( result .pointer("/contents/value") .unwrap() .to_string() .replace("\\r", ""), "\"# cmt\"" ); let resp = send_hover_request(&client_connection, script.clone(), 17, 27); let result = result_from_message(resp); assert_eq!( result .pointer("/contents/value") .unwrap() .to_string() .replace("\\r", ""), "\"# sub cmt\"" ); let resp = send_hover_request(&client_connection, script, 19, 33); let result = result_from_message(resp); assert_eq!( result .pointer("/contents/value") .unwrap() .to_string() .replace("\\r", ""), "\"# sub sub cmt\"" ); } #[test] fn hover_on_use_command() { let mut script = fixtures(); script.push("lsp"); script.push("hover"); script.push("use.nu"); let script_uri = path_to_uri(&script); let config = format!("use {}", script.to_str().unwrap()); let (client_connection, _recv) = initialize_language_server(Some(&config), None); open_unchecked(&client_connection, script_uri.clone()); let resp = send_hover_request(&client_connection, script_uri.clone(), 0, 19); let result = result_from_message(resp); assert_eq!( result .pointer("/contents/value") .unwrap() .to_string() .replace("\\r", ""), "\"```\\nrecord>\\n``` \\n---\\nimmutable\"" ); let resp = send_hover_request(&client_connection, script_uri, 0, 22); let result = result_from_message(resp); assert!(result .pointer("/contents/value") .unwrap() .to_string() .replace("\\r", "") .starts_with("\"\\n---\\n### Usage \\n```nu\\n foo {flags}\\n```\\n\\n### Flags")); } }