mirror of
https://github.com/nushell/nushell.git
synced 2025-04-11 14:58:21 +02:00
feat(lsp): hover on external command shows manpage (#15115)
# Description <img width="642" alt="image" src="https://github.com/user-attachments/assets/a97e4f33-df12-4240-a221-d4b97a171de0" /> Not particularly useful, but better than showing nothing I guess. #14464 Also fixed a markdown syntax issue for mutable variable hovering # User-Facing Changes # Tests + Formatting +1 # After Submitting --------- Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
parent
d4aeadbb44
commit
fb7b0a8c11
@ -19,6 +19,26 @@ fn strip_quotes(span: Span, working_set: &StateWorkingSet) -> Span {
|
||||
}
|
||||
}
|
||||
|
||||
fn try_find_id_in_misc(
|
||||
call: &Call,
|
||||
working_set: &StateWorkingSet,
|
||||
location: Option<&usize>,
|
||||
id_ref: Option<&Id>,
|
||||
) -> Option<(Id, Span)> {
|
||||
let call_name = working_set.get_span_contents(call.head);
|
||||
match call_name {
|
||||
b"def" | b"export def" => try_find_id_in_def(call, working_set, location, id_ref),
|
||||
b"module" | b"export module" => try_find_id_in_mod(call, working_set, location, id_ref),
|
||||
b"use" | b"export use" | b"hide" => {
|
||||
try_find_id_in_use(call, working_set, location, id_ref, call_name)
|
||||
}
|
||||
b"overlay use" | b"overlay hide" => {
|
||||
try_find_id_in_overlay(call, working_set, location, id_ref)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// For situations like
|
||||
/// ```nushell
|
||||
/// def foo [] {}
|
||||
@ -35,10 +55,6 @@ fn try_find_id_in_def(
|
||||
location: Option<&usize>,
|
||||
id_ref: Option<&Id>,
|
||||
) -> Option<(Id, Span)> {
|
||||
let call_name = working_set.get_span_contents(call.head);
|
||||
if call_name != b"def" && call_name != b"export def" {
|
||||
return None;
|
||||
};
|
||||
let mut span = None;
|
||||
for arg in call.arguments.iter() {
|
||||
if location.map_or(true, |pos| arg.span().contains(*pos)) {
|
||||
@ -80,10 +96,6 @@ fn try_find_id_in_mod(
|
||||
location: Option<&usize>,
|
||||
id_ref: Option<&Id>,
|
||||
) -> Option<(Id, Span)> {
|
||||
let call_name = working_set.get_span_contents(call.head);
|
||||
if call_name != b"module" && call_name != b"export module" {
|
||||
return None;
|
||||
};
|
||||
let check_location = |span: &Span| location.map_or(true, |pos| span.contains(*pos));
|
||||
|
||||
call.arguments.first().and_then(|arg| {
|
||||
@ -119,11 +131,8 @@ fn try_find_id_in_use(
|
||||
working_set: &StateWorkingSet,
|
||||
location: Option<&usize>,
|
||||
id: Option<&Id>,
|
||||
call_name: &[u8],
|
||||
) -> Option<(Id, Span)> {
|
||||
let call_name = working_set.get_span_contents(call.head);
|
||||
if call_name != b"use" && call_name != b"export use" && call_name != b"hide" {
|
||||
return None;
|
||||
}
|
||||
// TODO: for keyword `hide`, the decl/var is already hidden in working_set,
|
||||
// this function will always return None.
|
||||
let find_by_name = |name: &[u8]| match id {
|
||||
@ -223,10 +232,6 @@ fn try_find_id_in_overlay(
|
||||
location: Option<&usize>,
|
||||
id: Option<&Id>,
|
||||
) -> Option<(Id, Span)> {
|
||||
let call_name = working_set.get_span_contents(call.head);
|
||||
if call_name != b"overlay use" && call_name != b"overlay hide" {
|
||||
return None;
|
||||
}
|
||||
let check_location = |span: &Span| location.map_or(true, |pos| span.contains(*pos));
|
||||
let module_from_overlay_name = |name: &str, span: Span| {
|
||||
let found_id = Id::Module(working_set.find_overlay(name.as_bytes())?.origin);
|
||||
@ -292,19 +297,19 @@ fn find_id_in_expr(
|
||||
if call.head.contains(*location) {
|
||||
FindMapResult::Found((Id::Declaration(call.decl_id), call.head))
|
||||
} else {
|
||||
try_find_id_in_def(call, working_set, Some(location), None)
|
||||
.or(try_find_id_in_mod(call, working_set, Some(location), None))
|
||||
.or(try_find_id_in_use(call, working_set, Some(location), None))
|
||||
.or(try_find_id_in_overlay(
|
||||
call,
|
||||
working_set,
|
||||
Some(location),
|
||||
None,
|
||||
))
|
||||
try_find_id_in_misc(call, working_set, Some(location), None)
|
||||
.map(FindMapResult::Found)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
Expr::ExternalCall(head, _) => {
|
||||
if head.span.contains(*location) {
|
||||
if let Expr::GlobPattern(cmd, _) = &head.expr {
|
||||
return FindMapResult::Found((Id::External(cmd.clone()), head.span));
|
||||
}
|
||||
}
|
||||
FindMapResult::Continue
|
||||
}
|
||||
Expr::FullCellPath(fcp) => {
|
||||
if fcp.head.span.contains(*location) {
|
||||
FindMapResult::Continue
|
||||
@ -383,11 +388,7 @@ fn find_reference_by_id_in_expr(
|
||||
occurs.push(call.head);
|
||||
return Some(occurs);
|
||||
}
|
||||
if let Some((_, span_found)) = try_find_id_in_def(call, working_set, None, Some(id))
|
||||
.or(try_find_id_in_mod(call, working_set, None, Some(id)))
|
||||
.or(try_find_id_in_use(call, working_set, None, Some(id)))
|
||||
.or(try_find_id_in_overlay(call, working_set, None, Some(id)))
|
||||
{
|
||||
if let Some((_, span_found)) = try_find_id_in_misc(call, working_set, None, Some(id)) {
|
||||
occurs.push(span_found);
|
||||
}
|
||||
Some(occurs)
|
||||
|
@ -145,7 +145,7 @@ fn extract_inlay_hints_from_expression(
|
||||
|
||||
impl LanguageServer {
|
||||
pub(crate) fn get_inlay_hints(&mut self, params: &InlayHintParams) -> Option<Vec<InlayHint>> {
|
||||
Some(self.inlay_hints.get(¶ms.text_document.uri)?.clone())
|
||||
self.inlay_hints.get(¶ms.text_document.uri).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn extract_inlay_hints(
|
||||
|
@ -43,6 +43,7 @@ pub(crate) enum Id {
|
||||
Value(Type),
|
||||
Module(ModuleId),
|
||||
CellPath(VarId, Vec<PathMember>),
|
||||
External(String),
|
||||
}
|
||||
|
||||
pub struct LanguageServer {
|
||||
@ -594,14 +595,12 @@ impl LanguageServer {
|
||||
.const_val
|
||||
.clone()
|
||||
.and_then(|v| v.coerce_into_string().ok())
|
||||
.map(|s| format!("\n---\n{}", s))
|
||||
.unwrap_or_default();
|
||||
let contents = format!(
|
||||
"{} ```\n{}\n``` {}",
|
||||
if var.mutable { "mutable " } else { "" },
|
||||
var.ty,
|
||||
value
|
||||
);
|
||||
.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) => {
|
||||
@ -636,6 +635,20 @@ impl LanguageServer {
|
||||
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) => String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
Err(_) => format!("No command help found for {}", &cmd),
|
||||
};
|
||||
markdown_hover(manpage_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -929,7 +942,7 @@ mod tests {
|
||||
|
||||
assert_json_eq!(
|
||||
result_from_message(resp),
|
||||
serde_json::json!({ "contents": { "kind": "markdown", "value": " ```\ntable\n``` " } })
|
||||
serde_json::json!({ "contents": { "kind": "markdown", "value": "```\ntable\n``` \n---\nimmutable" } })
|
||||
);
|
||||
}
|
||||
|
||||
@ -991,6 +1004,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_on_external_command() {
|
||||
let (client_connection, _recv) = initialize_language_server(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.clone(), 6, 2);
|
||||
|
||||
let hover_text = result_from_message(resp)
|
||||
.pointer("/contents/value")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
#[cfg(not(windows))]
|
||||
assert!(hover_text.starts_with("SLEEP(1)"));
|
||||
#[cfg(windows)]
|
||||
assert!(hover_text.starts_with("NAME\r\n Start-Sleep"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_on_str_join() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
|
@ -274,9 +274,9 @@ impl LanguageServer {
|
||||
let uri = params.text_document.uri.to_owned();
|
||||
let docs = self.docs.lock().ok()?;
|
||||
self.symbol_cache.update(&uri, &engine_state, &docs);
|
||||
Some(DocumentSymbolResponse::Flat(
|
||||
self.symbol_cache.get_symbols_by_uri(&uri)?,
|
||||
))
|
||||
self.symbol_cache
|
||||
.get_symbols_by_uri(&uri)
|
||||
.map(DocumentSymbolResponse::Flat)
|
||||
}
|
||||
|
||||
pub(crate) fn workspace_symbol(
|
||||
|
3
tests/fixtures/lsp/hover/command.nu
vendored
3
tests/fixtures/lsp/hover/command.nu
vendored
@ -1,6 +1,7 @@
|
||||
# Renders some greeting message
|
||||
def hello [] {}
|
||||
def hello [] { }
|
||||
|
||||
hello
|
||||
|
||||
[""] | str join
|
||||
^sleep
|
||||
|
Loading…
Reference in New Issue
Block a user