fix(lsp): several edge cases of inaccurate references (#15523)

# Description

Sometimes recognizing identical concepts in nushell can be difficult.
This PR fixes some cases.

# User-Facing Changes

## Before:

<img width="317" alt="image"
src="https://github.com/user-attachments/assets/40567fd2-4cf4-44bb-8845-5f39935f41bb"
/>
<img width="317" alt="image"
src="https://github.com/user-attachments/assets/0cc21aab-8c8a-4bdd-adaf-70117e46c88d"
/>
<img width="276" alt="image"
src="https://github.com/user-attachments/assets/2820f958-b1aa-4bf1-b2ec-36e3191dd1aa"
/>
<img width="311" alt="image"
src="https://github.com/user-attachments/assets/407fb20f-ca5a-42a2-b0ac-791a7ee8497a"
/>

## After:

<img width="317" alt="image"
src="https://github.com/user-attachments/assets/91ca595f-36c5-4081-ba19-4800eb89cbec"
/>
<img width="317" alt="image"
src="https://github.com/user-attachments/assets/222aa0d1-b9c6-441c-8ecd-66ae91c7d397"
/>
<img width="275" alt="image"
src="https://github.com/user-attachments/assets/7b3122d3-ed5a-4bee-8e35-5ef01abc25a1"
/>
<img width="316" alt="image"
src="https://github.com/user-attachments/assets/2c026055-5962-4d4c-97d4-c453a2fef82b"
/>

# Tests + Formatting

+3

# After Submitting
This commit is contained in:
zc he 2025-04-10 10:15:35 +08:00 committed by GitHub
parent 15146e68ad
commit e4cef8a154
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 467 additions and 167 deletions

1
Cargo.lock generated
View File

@ -3821,6 +3821,7 @@ dependencies = [
"lsp-server", "lsp-server",
"lsp-textdocument", "lsp-textdocument",
"lsp-types", "lsp-types",
"memchr",
"miette", "miette",
"nu-cli", "nu-cli",
"nu-cmd-lang", "nu-cmd-lang",

View File

@ -18,6 +18,7 @@ crossbeam-channel = { workspace = true }
lsp-server = { workspace = true } lsp-server = { workspace = true }
lsp-textdocument = { workspace = true } lsp-textdocument = { workspace = true }
lsp-types = { workspace = true } lsp-types = { workspace = true }
memchr = { workspace = true }
miette = { workspace = true } miette = { workspace = true }
nucleo-matcher = { workspace = true } nucleo-matcher = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View File

@ -2,20 +2,25 @@ use crate::Id;
use nu_protocol::{ use nu_protocol::{
ast::{Argument, Block, Call, Expr, Expression, FindMapResult, ListItem, PathMember, Traverse}, ast::{Argument, Block, Call, Expr, Expression, FindMapResult, ListItem, PathMember, Traverse},
engine::StateWorkingSet, engine::StateWorkingSet,
Span, ModuleId, Span,
}; };
use std::sync::Arc; use std::sync::Arc;
/// Adjust span if quoted /// Adjust span if quoted
fn strip_quotes(span: Span, working_set: &StateWorkingSet) -> Span { fn strip_quotes(span: Span, working_set: &StateWorkingSet) -> (Box<[u8]>, Span) {
let text = String::from_utf8_lossy(working_set.get_span_contents(span)); let text = working_set.get_span_contents(span);
if text.len() > 1 if text.len() > 1
&& ((text.starts_with('"') && text.ends_with('"')) && ((text.starts_with(b"\"") && text.ends_with(b"\""))
|| (text.starts_with('\'') && text.ends_with('\''))) || (text.starts_with(b"'") && text.ends_with(b"'")))
{ {
Span::new(span.start.saturating_add(1), span.end.saturating_sub(1)) (
text.get(1..text.len() - 1)
.expect("Invalid quoted span!")
.into(),
Span::new(span.start.saturating_add(1), span.end.saturating_sub(1)),
)
} else { } else {
span (text.into(), span)
} }
} }
@ -53,6 +58,12 @@ fn try_find_id_in_def(
location: Option<&usize>, location: Option<&usize>,
id_ref: Option<&Id>, id_ref: Option<&Id>,
) -> Option<(Id, Span)> { ) -> Option<(Id, Span)> {
// skip if the id to search is not a declaration id
if let Some(id_ref) = id_ref {
if !matches!(id_ref, Id::Declaration(_)) {
return None;
}
}
let mut span = None; let mut span = None;
for arg in call.arguments.iter() { for arg in call.arguments.iter() {
if location.is_none_or(|pos| arg.span().contains(*pos)) { if location.is_none_or(|pos| arg.span().contains(*pos)) {
@ -70,9 +81,8 @@ fn try_find_id_in_def(
} }
} }
} }
let span = strip_quotes(span?, working_set); let (name, span) = strip_quotes(span?, working_set);
let name = working_set.get_span_contents(span); let decl_id = Id::Declaration(working_set.find_decl(&name).or_else(|| {
let decl_id = Id::Declaration(working_set.find_decl(name).or_else(|| {
// for defs inside def // for defs inside def
// TODO: get scope by position // TODO: get scope by position
// https://github.com/nushell/nushell/issues/15291 // https://github.com/nushell/nushell/issues/15291
@ -104,8 +114,14 @@ fn try_find_id_in_mod(
location: Option<&usize>, location: Option<&usize>,
id_ref: Option<&Id>, id_ref: Option<&Id>,
) -> Option<(Id, Span)> { ) -> Option<(Id, Span)> {
let check_location = |span: &Span| location.is_none_or(|pos| span.contains(*pos)); // skip if the id to search is not a module id
if let Some(id_ref) = id_ref {
if !matches!(id_ref, Id::Module(_, _)) {
return None;
}
}
let check_location = |span: &Span| location.is_none_or(|pos| span.contains(*pos));
call.arguments.first().and_then(|arg| { call.arguments.first().and_then(|arg| {
if !check_location(&arg.span()) { if !check_location(&arg.span()) {
return None; return None;
@ -113,9 +129,31 @@ fn try_find_id_in_mod(
match arg { match arg {
Argument::Positional(expr) => { Argument::Positional(expr) => {
let name = expr.as_string()?; let name = expr.as_string()?;
let module_id = working_set.find_module(name.as_bytes())?; let module_id = working_set.find_module(name.as_bytes()).or_else(|| {
let found_id = Id::Module(module_id); // in case the module is hidden
let found_span = strip_quotes(arg.span(), working_set); let mut any_id = true;
let mut id_num_ref = 0;
if let Some(Id::Module(id_ref, _)) = id_ref {
any_id = false;
id_num_ref = id_ref.get();
}
let block_span = call.arguments.last()?.span();
(0..working_set.num_modules())
.find(|id| {
(any_id || id_num_ref == *id)
&& working_set.get_module(ModuleId::new(*id)).span.is_some_and(
|mod_span| {
mod_span.start <= block_span.start + 1
&& block_span.start <= mod_span.start
&& block_span.end >= mod_span.end
&& block_span.end <= mod_span.end + 1
},
)
})
.map(ModuleId::new)
})?;
let found_id = Id::Module(module_id, name.as_bytes().into());
let found_span = strip_quotes(arg.span(), working_set).1;
id_ref id_ref
.is_none_or(|id_r| found_id == *id_r) .is_none_or(|id_r| found_id == *id_r)
.then_some((found_id, found_span)) .then_some((found_id, found_span))
@ -135,7 +173,7 @@ fn try_find_id_in_use(
call: &Call, call: &Call,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
location: Option<&usize>, location: Option<&usize>,
id: Option<&Id>, id_ref: Option<&Id>,
) -> Option<(Id, Span)> { ) -> Option<(Id, Span)> {
// NOTE: `call.parser_info` contains a 'import_pattern' field for `use`/`hide` commands, // NOTE: `call.parser_info` contains a 'import_pattern' field for `use`/`hide` commands,
// If it's missing, usually it means the PWD env is not correctly set, // If it's missing, usually it means the PWD env is not correctly set,
@ -151,25 +189,43 @@ fn try_find_id_in_use(
let find_by_name = |name: &[u8]| { let find_by_name = |name: &[u8]| {
let module = working_set.get_module(module_id); let module = working_set.get_module(module_id);
match id { match id_ref {
Some(Id::Variable(var_id_ref)) => module Some(Id::Variable(var_id_ref, name_ref)) => module
.constants .constants
.get(name) .get(name)
.and_then(|var_id| (*var_id == *var_id_ref).then_some(Id::Variable(*var_id))), .cloned()
.or_else(|| {
// NOTE: This is for the module record variable:
// https://www.nushell.sh/book/modules/using_modules.html#importing-constants
// The definition span is located at the head of the `use` command.
(name_ref.as_ref() == name
&& call
.head
.contains_span(working_set.get_variable(*var_id_ref).declaration_span))
.then_some(*var_id_ref)
})
.and_then(|var_id| {
(var_id == *var_id_ref).then_some(Id::Variable(var_id, name.into()))
}),
Some(Id::Declaration(decl_id_ref)) => module.decls.get(name).and_then(|decl_id| { Some(Id::Declaration(decl_id_ref)) => module.decls.get(name).and_then(|decl_id| {
(*decl_id == *decl_id_ref).then_some(Id::Declaration(*decl_id)) (*decl_id == *decl_id_ref).then_some(Id::Declaration(*decl_id))
}), }),
// this is only for argument `members` // this is only for argument `members`
Some(Id::Module(module_id_ref)) => module.submodules.get(name).and_then(|module_id| { Some(Id::Module(module_id_ref, name_ref)) => {
(*module_id == *module_id_ref).then_some(Id::Module(*module_id)) module.submodules.get(name).and_then(|module_id| {
}), (*module_id == *module_id_ref && name_ref.as_ref() == name)
.then_some(Id::Module(*module_id, name.into()))
})
}
None => module None => module
.submodules .submodules
.get(name) .get(name)
.cloned() .map(|id| Id::Module(*id, name.into()))
.map(Id::Module)
.or(module.decls.get(name).cloned().map(Id::Declaration)) .or(module.decls.get(name).cloned().map(Id::Declaration))
.or(module.constants.get(name).cloned().map(Id::Variable)), .or(module
.constants
.get(name)
.map(|id| Id::Variable(*id, name.into()))),
_ => None, _ => None,
} }
}; };
@ -178,16 +234,17 @@ fn try_find_id_in_use(
// Get module id if required // Get module id if required
let module_name = call.arguments.first()?; let module_name = call.arguments.first()?;
let span = module_name.span(); let span = module_name.span();
if let Some(Id::Module(id_ref)) = id { let (span_content, clean_span) = strip_quotes(span, working_set);
if let Some(Id::Module(id_ref, name_ref)) = id_ref {
// still need to check the rest, if id not matched // still need to check the rest, if id not matched
if module_id == *id_ref { if module_id == *id_ref && name_ref == &span_content {
return Some((Id::Module(module_id), strip_quotes(span, working_set))); return Some((Id::Module(module_id, span_content), clean_span));
} }
} }
if let Some(pos) = location { if let Some(pos) = location {
// first argument of `use`/`hide` should always be module name // first argument of `use`/`hide` should always be module name
if span.contains(*pos) { if span.contains(*pos) {
return Some((Id::Module(module_id), strip_quotes(span, working_set))); return Some((Id::Module(module_id, span_content), clean_span));
} }
} }
@ -200,14 +257,13 @@ fn try_find_id_in_use(
let name = e.as_string()?; let name = e.as_string()?;
Some(( Some((
find_by_name(name.as_bytes())?, find_by_name(name.as_bytes())?,
strip_quotes(item_expr.span, working_set), strip_quotes(item_expr.span, working_set).1,
)) ))
}) })
}) })
}; };
let arguments = call.arguments.get(1..)?; for arg in call.arguments.get(1..)?.iter().rev() {
for arg in arguments {
let Argument::Positional(expr) = arg else { let Argument::Positional(expr) = arg else {
continue; continue;
}; };
@ -216,7 +272,7 @@ fn try_find_id_in_use(
} }
let matched = match &expr.expr { let matched = match &expr.expr {
Expr::String(name) => { Expr::String(name) => {
find_by_name(name.as_bytes()).map(|id| (id, strip_quotes(expr.span, working_set))) find_by_name(name.as_bytes()).map(|id| (id, strip_quotes(expr.span, working_set).1))
} }
Expr::List(items) => search_in_list_items(items), Expr::List(items) => search_in_list_items(items),
Expr::FullCellPath(fcp) => { Expr::FullCellPath(fcp) => {
@ -245,10 +301,16 @@ fn try_find_id_in_overlay(
call: &Call, call: &Call,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
location: Option<&usize>, location: Option<&usize>,
id: Option<&Id>, id_ref: Option<&Id>,
) -> Option<(Id, Span)> { ) -> Option<(Id, Span)> {
// skip if the id to search is not a module id
if let Some(id_ref) = id_ref {
if !matches!(id_ref, Id::Module(_, _)) {
return None;
}
}
let check_location = |span: &Span| location.is_none_or(|pos| span.contains(*pos)); let check_location = |span: &Span| location.is_none_or(|pos| span.contains(*pos));
let module_from_parser_info = |span: Span| { let module_from_parser_info = |span: Span, name: &str| {
let Expression { let Expression {
expr: Expr::Overlay(Some(module_id)), expr: Expr::Overlay(Some(module_id)),
.. ..
@ -256,18 +318,24 @@ fn try_find_id_in_overlay(
else { else {
return None; return None;
}; };
let found_id = Id::Module(*module_id); let found_id = Id::Module(*module_id, name.as_bytes().into());
id.is_none_or(|id_r| found_id == *id_r) id_ref
.then_some((found_id, strip_quotes(span, working_set))) .is_none_or(|id_r| found_id == *id_r)
.then_some((found_id, strip_quotes(span, working_set).1))
}; };
// NOTE: `overlay_expr` doesn't work for `overlay hide` // NOTE: `overlay_expr` doesn't exist for `overlay hide`
let module_from_overlay_name = |name: &str, span: Span| { let module_from_overlay_name = |name: &str, span: Span| {
let found_id = Id::Module(working_set.find_overlay(name.as_bytes())?.origin); let found_id = Id::Module(
id.is_none_or(|id_r| found_id == *id_r) working_set.find_overlay(name.as_bytes())?.origin,
.then_some((found_id, strip_quotes(span, working_set))) name.as_bytes().into(),
);
id_ref
.is_none_or(|id_r| found_id == *id_r)
.then_some((found_id, strip_quotes(span, working_set).1))
}; };
for arg in call.arguments.iter() { // check `as alias` first
for arg in call.arguments.iter().rev() {
let Argument::Positional(expr) = arg else { let Argument::Positional(expr) = arg else {
continue; continue;
}; };
@ -275,11 +343,11 @@ fn try_find_id_in_overlay(
continue; continue;
}; };
let matched = match &expr.expr { let matched = match &expr.expr {
Expr::String(name) => module_from_parser_info(expr.span) Expr::String(name) => module_from_parser_info(expr.span, name)
.or_else(|| module_from_overlay_name(name, expr.span)), .or_else(|| module_from_overlay_name(name, expr.span)),
// keyword 'as' // keyword 'as'
Expr::Keyword(kwd) => match &kwd.expr.expr { Expr::Keyword(kwd) => match &kwd.expr.expr {
Expr::String(name) => module_from_parser_info(kwd.expr.span) Expr::String(name) => module_from_parser_info(kwd.expr.span, name)
.or_else(|| module_from_overlay_name(name, kwd.expr.span)), .or_else(|| module_from_overlay_name(name, kwd.expr.span)),
_ => None, _ => None,
}, },
@ -292,6 +360,19 @@ fn try_find_id_in_overlay(
None None
} }
/// Trim leading `$` sign For variable references `$foo`
fn strip_dollar_sign(span: Span, working_set: &StateWorkingSet<'_>) -> (Box<[u8]>, Span) {
let content = working_set.get_span_contents(span);
if content.starts_with(b"$") {
(
content[1..].into(),
Span::new(span.start.saturating_add(1), span.end),
)
} else {
(content.into(), span)
}
}
fn find_id_in_expr( fn find_id_in_expr(
expr: &Expression, expr: &Expression,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
@ -303,12 +384,10 @@ fn find_id_in_expr(
} }
let span = expr.span; let span = expr.span;
match &expr.expr { match &expr.expr {
Expr::VarDecl(var_id) => FindMapResult::Found((Id::Variable(*var_id), span)), Expr::VarDecl(var_id) | Expr::Var(var_id) => {
// trim leading `$` sign let (name, clean_span) = strip_dollar_sign(span, working_set);
Expr::Var(var_id) => FindMapResult::Found(( FindMapResult::Found((Id::Variable(*var_id, name), clean_span))
Id::Variable(*var_id), }
Span::new(span.start.saturating_add(1), span.end),
)),
Expr::Call(call) => { Expr::Call(call) => {
if call.head.contains(*location) { if call.head.contains(*location) {
FindMapResult::Found((Id::Declaration(call.decl_id), call.head)) FindMapResult::Found((Id::Declaration(call.decl_id), call.head))
@ -349,7 +428,9 @@ fn find_id_in_expr(
FindMapResult::Found((Id::CellPath(var_id, tail), span)) FindMapResult::Found((Id::CellPath(var_id, tail), span))
} }
} }
Expr::Overlay(Some(module_id)) => FindMapResult::Found((Id::Module(*module_id), span)), Expr::Overlay(Some(module_id)) => {
FindMapResult::Found((Id::Module(*module_id, [].into()), span))
}
// terminal value expressions // terminal value expressions
Expr::Bool(_) Expr::Bool(_)
| Expr::Binary(_) | Expr::Binary(_)
@ -385,12 +466,12 @@ fn find_reference_by_id_in_expr(
) -> Option<Vec<Span>> { ) -> Option<Vec<Span>> {
let closure = |e| find_reference_by_id_in_expr(e, working_set, id); let closure = |e| find_reference_by_id_in_expr(e, working_set, id);
match (&expr.expr, id) { match (&expr.expr, id) {
(Expr::Var(vid1), Id::Variable(vid2)) if *vid1 == *vid2 => Some(vec![Span::new( (Expr::Var(vid1), Id::Variable(vid2, _)) if *vid1 == *vid2 => Some(vec![Span::new(
// we want to exclude the `$` sign for renaming // we want to exclude the `$` sign for renaming
expr.span.start.saturating_add(1), expr.span.start.saturating_add(1),
expr.span.end, expr.span.end,
)]), )]),
(Expr::VarDecl(vid1), Id::Variable(vid2)) if *vid1 == *vid2 => Some(vec![expr.span]), (Expr::VarDecl(vid1), Id::Variable(vid2, _)) if *vid1 == *vid2 => Some(vec![expr.span]),
// also interested in `var_id` in call.arguments of `use` command // also interested in `var_id` in call.arguments of `use` command
// and `module_id` in `module` command // and `module_id` in `module` command
(Expr::Call(call), _) => { (Expr::Call(call), _) => {

View File

@ -588,7 +588,7 @@ mod tests {
]) ])
); );
let resp = send_complete_request(&client_connection, script.clone(), 7, 15); let resp = send_complete_request(&client_connection, script, 7, 15);
assert_json_include!( assert_json_include!(
actual: result_from_message(resp), actual: result_from_message(resp),
expected: serde_json::json!([ expected: serde_json::json!([

View File

@ -51,11 +51,11 @@ impl LanguageServer {
let block_id = working_set.get_decl(*decl_id).block_id()?; let block_id = working_set.get_decl(*decl_id).block_id()?;
working_set.get_block(block_id).span working_set.get_block(block_id).span
} }
Id::Variable(var_id) => { Id::Variable(var_id, _) => {
let var = working_set.get_variable(*var_id); let var = working_set.get_variable(*var_id);
Some(var.declaration_span) Some(var.declaration_span)
} }
Id::Module(module_id) => { Id::Module(module_id, _) => {
let module = working_set.get_module(*module_id); let module = working_set.get_module(*module_id);
module.span module.span
} }
@ -147,7 +147,7 @@ mod tests {
let mut none_existent_path = root(); let mut none_existent_path = root();
none_existent_path.push("none-existent.nu"); none_existent_path.push("none-existent.nu");
let script = path_to_uri(&none_existent_path); let script = path_to_uri(&none_existent_path);
let resp = send_goto_definition_request(&client_connection, script.clone(), 0, 0); let resp = send_goto_definition_request(&client_connection, script, 0, 0);
assert_json_eq!(result_from_message(resp), serde_json::json!(null)); assert_json_eq!(result_from_message(resp), serde_json::json!(null));
} }
@ -195,7 +195,7 @@ mod tests {
serde_json::json!({ "line": 1, "character": 10 }) serde_json::json!({ "line": 1, "character": 10 })
); );
let resp = send_goto_definition_request(&client_connection, script.clone(), 2, 9); let resp = send_goto_definition_request(&client_connection, script, 2, 9);
assert_json_eq!( assert_json_eq!(
result_from_message(resp).pointer("/range/start").unwrap(), result_from_message(resp).pointer("/range/start").unwrap(),
serde_json::json!({ "line": 1, "character": 17 }) serde_json::json!({ "line": 1, "character": 17 })
@ -451,7 +451,7 @@ mod tests {
serde_json::json!({ "line": 0, "character": 0 }) serde_json::json!({ "line": 0, "character": 0 })
); );
let resp = send_goto_definition_request(&client_connection, script.clone(), 2, 30); let resp = send_goto_definition_request(&client_connection, script, 2, 30);
assert_json_eq!( assert_json_eq!(
result_from_message(resp).pointer("/range/start").unwrap(), result_from_message(resp).pointer("/range/start").unwrap(),
serde_json::json!({ "line": 0, "character": 0 }) serde_json::json!({ "line": 0, "character": 0 })

View File

@ -213,7 +213,7 @@ mod tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_inlay_hint_request(&client_connection, script.clone()); let resp = send_inlay_hint_request(&client_connection, script);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -240,7 +240,7 @@ mod tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_inlay_hint_request(&client_connection, script.clone()); let resp = send_inlay_hint_request(&client_connection, script);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -268,7 +268,7 @@ mod tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_inlay_hint_request(&client_connection, script.clone()); let resp = send_inlay_hint_request(&client_connection, script);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -333,7 +333,7 @@ mod tests {
let (client_connection, _recv) = initialize_language_server(Some(&config), None); let (client_connection, _recv) = initialize_language_server(Some(&config), None);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_inlay_hint_request(&client_connection, script.clone()); let resp = send_inlay_hint_request(&client_connection, script);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),

View File

@ -142,7 +142,7 @@ impl LanguageServer {
}; };
match id { match id {
Id::Variable(var_id) => { Id::Variable(var_id, _) => {
let var = working_set.get_variable(var_id); let var = working_set.get_variable(var_id);
let value = var let value = var
.const_val .const_val
@ -178,7 +178,7 @@ impl LanguageServer {
working_set.get_decl(decl_id), working_set.get_decl(decl_id),
false, false,
)), )),
Id::Module(module_id) => { Id::Module(module_id, _) => {
let description = working_set let description = working_set
.get_module_comments(module_id)? .get_module_comments(module_id)?
.iter() .iter()
@ -231,7 +231,7 @@ mod hover_tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_hover_request(&client_connection, script.clone(), 2, 0); let resp = send_hover_request(&client_connection, script, 2, 0);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -248,7 +248,6 @@ mod hover_tests {
script.push("hover"); script.push("hover");
script.push("use.nu"); script.push("use.nu");
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_hover_request(&client_connection, script.clone(), 2, 3); let resp = send_hover_request(&client_connection, script.clone(), 2, 3);
@ -265,12 +264,27 @@ mod hover_tests {
serde_json::json!("```\nrecord<bar: int>\n```") serde_json::json!("```\nrecord<bar: int>\n```")
); );
let resp = send_hover_request(&client_connection, script.clone(), 2, 11); let resp = send_hover_request(&client_connection, script, 2, 11);
let result = result_from_message(resp); let result = result_from_message(resp);
assert_json_eq!( assert_json_eq!(
result.pointer("/contents/value").unwrap(), result.pointer("/contents/value").unwrap(),
serde_json::json!("```\nint\n```\n---\n2") 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] #[test]
@ -284,7 +298,7 @@ mod hover_tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_hover_request(&client_connection, script.clone(), 3, 0); let resp = send_hover_request(&client_connection, script, 3, 0);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -308,7 +322,7 @@ mod hover_tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_hover_request(&client_connection, script.clone(), 9, 7); let resp = send_hover_request(&client_connection, script, 9, 7);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -332,7 +346,7 @@ mod hover_tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_hover_request(&client_connection, script.clone(), 6, 2); let resp = send_hover_request(&client_connection, script, 6, 2);
let hover_text = result_from_message(resp) let hover_text = result_from_message(resp)
.pointer("/contents/value") .pointer("/contents/value")
@ -358,7 +372,7 @@ mod hover_tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_hover_request(&client_connection, script.clone(), 5, 8); let resp = send_hover_request(&client_connection, script, 5, 8);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -377,21 +391,42 @@ mod hover_tests {
let mut script = fixtures(); let mut script = fixtures();
script.push("lsp"); script.push("lsp");
script.push("goto"); script.push("workspace");
script.push("module.nu"); script.push("foo.nu");
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_hover_request(&client_connection, script.clone(), 3, 12); let resp = send_hover_request(&client_connection, script.clone(), 15, 15);
let result = result_from_message(resp); let result = result_from_message(resp);
assert_eq!( assert_eq!(
result result
.pointer("/contents/value") .pointer("/contents/value")
.unwrap() .unwrap()
.to_string() .to_string()
.replace("\\r", ""), .replace("\\r", ""),
"\"# module doc\"" "\"# 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\""
); );
} }
@ -419,7 +454,7 @@ mod hover_tests {
"\"```\\nrecord<foo: list<any>>\\n``` \\n---\\nimmutable\"" "\"```\\nrecord<foo: list<any>>\\n``` \\n---\\nimmutable\""
); );
let resp = send_hover_request(&client_connection, script_uri.clone(), 0, 22); let resp = send_hover_request(&client_connection, script_uri, 0, 22);
let result = result_from_message(resp); let result = result_from_message(resp);
assert!(result assert!(result

View File

@ -40,10 +40,10 @@ mod workspace;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub(crate) enum Id { pub(crate) enum Id {
Variable(VarId), Variable(VarId, Box<[u8]>),
Declaration(DeclId), Declaration(DeclId),
Value(Type), Value(Type),
Module(ModuleId), Module(ModuleId, Box<[u8]>),
CellPath(VarId, Vec<PathMember>), CellPath(VarId, Vec<PathMember>),
External(String), External(String),
} }
@ -352,7 +352,7 @@ impl LanguageServer {
if self.need_parse { if self.need_parse {
// TODO: incremental parsing // TODO: incremental parsing
// add block to working_set for later references // add block to working_set for later references
working_set.add_block(block.clone()); working_set.add_block(block);
self.cached_state_delta = Some(working_set.delta.clone()); self.cached_state_delta = Some(working_set.delta.clone());
self.need_parse = false; self.need_parse = false;
} }
@ -615,7 +615,7 @@ mod tests {
method: DidChangeTextDocument::METHOD.to_string(), method: DidChangeTextDocument::METHOD.to_string(),
params: serde_json::to_value(DidChangeTextDocumentParams { params: serde_json::to_value(DidChangeTextDocumentParams {
text_document: lsp_types::VersionedTextDocumentIdentifier { text_document: lsp_types::VersionedTextDocumentIdentifier {
uri: uri.clone(), uri,
version: 2, version: 2,
}, },
content_changes: vec![TextDocumentContentChangeEvent { content_changes: vec![TextDocumentContentChangeEvent {

View File

@ -19,20 +19,19 @@ impl LanguageServer {
docs.listen(notification.method.as_str(), &notification.params); docs.listen(notification.method.as_str(), &notification.params);
match notification.method.as_str() { match notification.method.as_str() {
DidOpenTextDocument::METHOD => { DidOpenTextDocument::METHOD => {
let params: DidOpenTextDocumentParams = let params: DidOpenTextDocumentParams = serde_json::from_value(notification.params)
serde_json::from_value(notification.params.clone()) .expect("Expect receive DidOpenTextDocumentParams");
.expect("Expect receive DidOpenTextDocumentParams");
Some(params.text_document.uri) Some(params.text_document.uri)
} }
DidChangeTextDocument::METHOD => { DidChangeTextDocument::METHOD => {
let params: DidChangeTextDocumentParams = let params: DidChangeTextDocumentParams =
serde_json::from_value(notification.params.clone()) serde_json::from_value(notification.params)
.expect("Expect receive DidChangeTextDocumentParams"); .expect("Expect receive DidChangeTextDocumentParams");
Some(params.text_document.uri) Some(params.text_document.uri)
} }
DidCloseTextDocument::METHOD => { DidCloseTextDocument::METHOD => {
let params: DidCloseTextDocumentParams = let params: DidCloseTextDocumentParams =
serde_json::from_value(notification.params.clone()) serde_json::from_value(notification.params)
.expect("Expect receive DidCloseTextDocumentParams"); .expect("Expect receive DidCloseTextDocumentParams");
let uri = params.text_document.uri; let uri = params.text_document.uri;
self.symbol_cache.drop(&uri); self.symbol_cache.drop(&uri);
@ -41,7 +40,7 @@ impl LanguageServer {
} }
DidChangeWorkspaceFolders::METHOD => { DidChangeWorkspaceFolders::METHOD => {
let params: DidChangeWorkspaceFoldersParams = let params: DidChangeWorkspaceFoldersParams =
serde_json::from_value(notification.params.clone()) serde_json::from_value(notification.params)
.expect("Expect receive DidChangeWorkspaceFoldersParams"); .expect("Expect receive DidChangeWorkspaceFoldersParams");
for added in params.event.added { for added in params.event.added {
self.workspace_folders.insert(added.name.clone(), added); self.workspace_folders.insert(added.name.clone(), added);
@ -155,7 +154,7 @@ mod tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_hover_request(&client_connection, script.clone(), 0, 0); let resp = send_hover_request(&client_connection, script, 0, 0);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -190,7 +189,7 @@ hello"#,
), ),
None, None,
); );
let resp = send_hover_request(&client_connection, script.clone(), 3, 0); let resp = send_hover_request(&client_connection, script, 3, 0);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),
@ -229,7 +228,7 @@ hello"#,
}, },
}), }),
); );
let resp = send_hover_request(&client_connection, script.clone(), 3, 0); let resp = send_hover_request(&client_connection, script, 3, 0);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),

View File

@ -144,7 +144,7 @@ mod tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_semantic_token_request(&client_connection, script.clone()); let resp = send_semantic_token_request(&client_connection, script);
assert_json_eq!( assert_json_eq!(
result_from_message(resp), result_from_message(resp),

View File

@ -110,7 +110,7 @@ impl SymbolCache {
range: span_to_range(&span, doc, doc_span.start), range: span_to_range(&span, doc, doc_span.start),
}) })
} }
Id::Variable(var_id) => { Id::Variable(var_id, _) => {
let var = working_set.get_variable(var_id); let var = working_set.get_variable(var_id);
let span = var.declaration_span; let span = var.declaration_span;
if !doc_span.contains(span.start) || span.end == span.start { if !doc_span.contains(span.start) || span.end == span.start {
@ -124,7 +124,7 @@ impl SymbolCache {
range, range,
}) })
} }
Id::Module(module_id) => { Id::Module(module_id, _) => {
let module = working_set.get_module(module_id); let module = working_set.get_module(module_id);
let span = module.span?; let span = module.span?;
if !doc_span.contains(span.start) { if !doc_span.contains(span.start) {
@ -157,7 +157,7 @@ impl SymbolCache {
.chain((0..working_set.num_vars()).filter_map(|id| { .chain((0..working_set.num_vars()).filter_map(|id| {
Self::get_symbol_by_id( Self::get_symbol_by_id(
working_set, working_set,
Id::Variable(VarId::new(id)), Id::Variable(VarId::new(id), [].into()),
doc, doc,
&cached_file.covered_span, &cached_file.covered_span,
) )
@ -165,7 +165,7 @@ impl SymbolCache {
.chain((0..working_set.num_modules()).filter_map(|id| { .chain((0..working_set.num_modules()).filter_map(|id| {
Self::get_symbol_by_id( Self::get_symbol_by_id(
working_set, working_set,
Id::Module(ModuleId::new(id)), Id::Module(ModuleId::new(id), [].into()),
doc, doc,
&cached_file.covered_span, &cached_file.covered_span,
) )
@ -236,7 +236,7 @@ impl SymbolCache {
self.cache self.cache
.get(uri)? .get(uri)?
.iter() .iter()
.map(|s| s.clone().to_symbol_information(uri)) .map(|s| s.to_symbol_information(uri))
.collect(), .collect(),
) )
} }
@ -250,7 +250,7 @@ impl SymbolCache {
); );
self.cache self.cache
.iter() .iter()
.flat_map(|(uri, symbols)| symbols.iter().map(|s| s.clone().to_symbol_information(uri))) .flat_map(|(uri, symbols)| symbols.iter().map(|s| s.to_symbol_information(uri)))
.filter_map(|s| { .filter_map(|s| {
let mut buf = Vec::new(); let mut buf = Vec::new();
let name = Utf32Str::new(&s.name, &mut buf); let name = Utf32Str::new(&s.name, &mut buf);
@ -361,7 +361,7 @@ mod tests {
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let resp = send_document_symbol_request(&client_connection, script.clone()); let resp = send_document_symbol_request(&client_connection, script);
assert_json_eq!(result_from_message(resp), serde_json::json!([])); assert_json_eq!(result_from_message(resp), serde_json::json!([]));
} }
@ -470,7 +470,7 @@ mod tests {
script.push("bar.nu"); script.push("bar.nu");
let script_bar = path_to_uri(&script); let script_bar = path_to_uri(&script);
open_unchecked(&client_connection, script_foo.clone()); open_unchecked(&client_connection, script_foo);
open_unchecked(&client_connection, script_bar.clone()); open_unchecked(&client_connection, script_bar.clone());
let resp = send_workspace_symbol_request(&client_connection, "br".to_string()); let resp = send_workspace_symbol_request(&client_connection, "br".to_string());
@ -531,7 +531,7 @@ mod tests {
let script_bar = path_to_uri(&script); let script_bar = path_to_uri(&script);
open_unchecked(&client_connection, script_foo.clone()); open_unchecked(&client_connection, script_foo.clone());
open_unchecked(&client_connection, script_bar.clone()); open_unchecked(&client_connection, script_bar);
let resp = send_workspace_symbol_request(&client_connection, "foo".to_string()); let resp = send_workspace_symbol_request(&client_connection, "foo".to_string());
assert_json_eq!( assert_json_eq!(

View File

@ -59,13 +59,29 @@ struct IDTracker {
/// Span of the original instance under the cursor /// Span of the original instance under the cursor
pub span: Span, pub span: Span,
/// Name of the definition /// Name of the definition
pub name: String, pub name: Box<[u8]>,
/// Span of the original file where the request comes from /// Span of the original file where the request comes from
pub file_span: Span, pub file_span: Span,
/// The redundant parsing should only happen once /// The redundant parsing should only happen once
pub renewed: bool, pub renewed: bool,
} }
impl IDTracker {
fn new(id: Id, span: Span, file_span: Span, working_set: &StateWorkingSet) -> Self {
let name = match &id {
Id::Variable(_, name) | Id::Module(_, name) => name.clone(),
_ => working_set.get_span_contents(span).into(),
};
Self {
id,
span,
name,
file_span,
renewed: false,
}
}
}
impl LanguageServer { impl LanguageServer {
/// Get initial workspace folders from initialization response /// Get initial workspace folders from initialization response
pub(crate) fn initialize_workspace_folders( pub(crate) fn initialize_workspace_folders(
@ -121,7 +137,6 @@ impl LanguageServer {
/// The rename request only happens after the client received a `PrepareRenameResponse`, /// The rename request only happens after the client received a `PrepareRenameResponse`,
/// and a new name typed in, could happen before ranges ready for all files in the workspace folder /// and a new name typed in, could happen before ranges ready for all files in the workspace folder
pub(crate) fn rename(&mut self, params: &RenameParams) -> Option<WorkspaceEdit> { pub(crate) fn rename(&mut self, params: &RenameParams) -> Option<WorkspaceEdit> {
let new_name = params.new_name.to_owned();
// changes in WorkspaceEdit have mutable key // changes in WorkspaceEdit have mutable key
#[allow(clippy::mutable_key_type)] #[allow(clippy::mutable_key_type)]
let changes: HashMap<Uri, Vec<TextEdit>> = self let changes: HashMap<Uri, Vec<TextEdit>> = self
@ -134,7 +149,7 @@ impl LanguageServer {
.iter() .iter()
.map(|range| TextEdit { .map(|range| TextEdit {
range: *range, range: *range,
new_text: new_name.clone(), new_text: params.new_name.to_owned(),
}) })
.collect(), .collect(),
) )
@ -176,16 +191,9 @@ impl LanguageServer {
.to_owned() .to_owned()
.unwrap_or(ProgressToken::Number(1)); .unwrap_or(ProgressToken::Number(1));
let id_tracker = IDTracker { let id_tracker = IDTracker::new(id, span, file_span, &working_set);
id,
span,
file_span,
name: String::from_utf8_lossy(working_set.get_span_contents(span)).to_string(),
renewed: false,
};
// make sure the parsing result of current file is merged in the state // make sure the parsing result of current file is merged in the state
let engine_state = self.new_engine_state(Some(&path_uri)); let engine_state = self.new_engine_state(Some(&path_uri));
self.channels = self self.channels = self
.find_reference_in_workspace( .find_reference_in_workspace(
engine_state, engine_state,
@ -270,16 +278,9 @@ impl LanguageServer {
.get_workspace_folder_by_uri(&path_uri) .get_workspace_folder_by_uri(&path_uri)
.ok_or_else(|| miette!("\nCurrent file is not in any workspace"))?; .ok_or_else(|| miette!("\nCurrent file is not in any workspace"))?;
// now continue parsing on other files in the workspace // now continue parsing on other files in the workspace
let id_tracker = IDTracker { let id_tracker = IDTracker::new(id, span, file_span, &working_set);
id,
span,
file_span,
name: String::from_utf8_lossy(working_set.get_span_contents(span)).to_string(),
renewed: false,
};
// make sure the parsing result of current file is merged in the state // make sure the parsing result of current file is merged in the state
let engine_state = self.new_engine_state(Some(&path_uri)); let engine_state = self.new_engine_state(Some(&path_uri));
self.channels = self self.channels = self
.find_reference_in_workspace( .find_reference_in_workspace(
engine_state, engine_state,
@ -338,18 +339,20 @@ impl LanguageServer {
file_span: Span, file_span: Span,
sample_span: Span, sample_span: Span,
) -> Option<Span> { ) -> Option<Span> {
if let (Id::Variable(_), Some(decl_span)) = (&id, definition_span) { if let (Id::Variable(_, name_ref), Some(decl_span)) = (&id, definition_span) {
if file_span.contains_span(decl_span) && decl_span.end > decl_span.start { if file_span.contains_span(decl_span) && decl_span.end > decl_span.start {
let leading_dashes = working_set let content = working_set.get_span_contents(decl_span);
.get_span_contents(decl_span) let leading_dashes = content
.iter() .iter()
// remove leading dashes for flags // remove leading dashes for flags
.take_while(|c| *c == &b'-') .take_while(|c| *c == &b'-')
.count(); .count();
let start = decl_span.start + leading_dashes; let start = decl_span.start + leading_dashes;
return Some(Span { return content.get(leading_dashes..).and_then(|name| {
start, name.starts_with(name_ref).then_some(Span {
end: start + sample_span.end - sample_span.start, start,
end: start + sample_span.end - sample_span.start,
})
}); });
} }
} }
@ -391,6 +394,8 @@ impl LanguageServer {
.collect(); .collect();
let len = scripts.len(); let len = scripts.len();
let definition_span = Self::find_definition_span_by_id(&working_set, &id_tracker.id); let definition_span = Self::find_definition_span_by_id(&working_set, &id_tracker.id);
let bytes_to_search = id_tracker.name.to_owned();
let finder = memchr::memmem::Finder::new(&bytes_to_search);
for (i, fp) in scripts.iter().enumerate() { for (i, fp) in scripts.iter().enumerate() {
#[cfg(test)] #[cfg(test)]
@ -411,7 +416,7 @@ impl LanguageServer {
let file = if let Some(file) = docs.get_document(&uri) { let file = if let Some(file) = docs.get_document(&uri) {
file file
} else { } else {
let bytes = match fs::read(fp) { let file_bytes = match fs::read(fp) {
Ok(it) => it, Ok(it) => it,
Err(_) => { Err(_) => {
// continue on fs error // continue on fs error
@ -419,15 +424,18 @@ impl LanguageServer {
} }
}; };
// skip if the file does not contain what we're looking for // skip if the file does not contain what we're looking for
let content_string = String::from_utf8_lossy(&bytes); if finder.find(&file_bytes).is_none() {
if !content_string.contains(&id_tracker.name) {
// progress without any data // progress without any data
data_sender data_sender
.send(InternalMessage::OnGoing(token.clone(), percentage)) .send(InternalMessage::OnGoing(token.clone(), percentage))
.into_diagnostic()?; .into_diagnostic()?;
continue; continue;
} }
&FullTextDocument::new("nu".to_string(), 0, content_string.into()) &FullTextDocument::new(
"nu".to_string(),
0,
String::from_utf8_lossy(&file_bytes).into(),
)
}; };
let _ = Self::find_reference_in_file(&mut working_set, file, fp, &mut id_tracker) let _ = Self::find_reference_in_file(&mut working_set, file, fp, &mut id_tracker)
.map(|mut refs| { .map(|mut refs| {
@ -458,7 +466,7 @@ impl LanguageServer {
}); });
} }
data_sender data_sender
.send(InternalMessage::Finished(token.clone())) .send(InternalMessage::Finished(token))
.into_diagnostic()?; .into_diagnostic()?;
Ok(()) Ok(())
}); });
@ -551,7 +559,7 @@ mod tests {
// use a hover request to interrupt // use a hover request to interrupt
if immediate_cancellation { if immediate_cancellation {
send_hover_request(client_connection, uri.clone(), line, character); send_hover_request(client_connection, uri, line, character);
} }
(0..num) (0..num)
@ -635,7 +643,7 @@ mod tests {
script.push("foo.nu"); script.push("foo.nu");
let script = path_to_uri(&script); let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script);
} }
#[test] #[test]
@ -659,14 +667,16 @@ mod tests {
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let message_num = 5; let message_num = 6;
let messages = let messages =
send_reference_request(&client_connection, script.clone(), 0, 12, message_num); send_reference_request(&client_connection, script.clone(), 0, 12, message_num);
assert_eq!(messages.len(), message_num); assert_eq!(messages.len(), message_num);
let mut has_response = false;
for message in messages { for message in messages {
match message { match message {
Message::Notification(n) => assert_eq!(n.method, "$/progress"), Message::Notification(n) => assert_eq!(n.method, "$/progress"),
Message::Response(r) => { Message::Response(r) => {
has_response = true;
let result = r.result.unwrap(); let result = r.result.unwrap();
let array = result.as_array().unwrap(); let array = result.as_array().unwrap();
assert!(array.contains(&serde_json::json!( assert!(array.contains(&serde_json::json!(
@ -678,7 +688,7 @@ mod tests {
)); ));
assert!(array.contains(&serde_json::json!( assert!(array.contains(&serde_json::json!(
{ {
"uri": script.to_string(), "uri": script,
"range": { "start": { "line": 0, "character": 11 }, "end": { "line": 0, "character": 16 } } "range": { "start": { "line": 0, "character": 11 }, "end": { "line": 0, "character": 16 } }
} }
) )
@ -687,6 +697,7 @@ mod tests {
_ => panic!("unexpected message type"), _ => panic!("unexpected message type"),
} }
} }
assert!(has_response);
} }
#[test] #[test]
@ -717,19 +728,21 @@ mod tests {
// thus changing the cached `StateDelta` // thus changing the cached `StateDelta`
open_unchecked(&client_connection, script_foo); open_unchecked(&client_connection, script_foo);
let message_num = 5; let message_num = 6;
let messages = let messages =
send_reference_request(&client_connection, script.clone(), 0, 23, message_num); send_reference_request(&client_connection, script.clone(), 0, 23, message_num);
assert_eq!(messages.len(), message_num); assert_eq!(messages.len(), message_num);
let mut has_response = false;
for message in messages { for message in messages {
match message { match message {
Message::Notification(n) => assert_eq!(n.method, "$/progress"), Message::Notification(n) => assert_eq!(n.method, "$/progress"),
Message::Response(r) => { Message::Response(r) => {
has_response = true;
let result = r.result.unwrap(); let result = r.result.unwrap();
let array = result.as_array().unwrap(); let array = result.as_array().unwrap();
assert!(array.contains(&serde_json::json!( assert!(array.contains(&serde_json::json!(
{ {
"uri": script.to_string(), "uri": script,
"range": { "start": { "line": 5, "character": 4 }, "end": { "line": 5, "character": 11 } } "range": { "start": { "line": 5, "character": 4 }, "end": { "line": 5, "character": 11 } }
} }
) )
@ -745,6 +758,61 @@ mod tests {
_ => panic!("unexpected message type"), _ => panic!("unexpected message type"),
} }
} }
assert!(has_response);
}
#[test]
fn module_path_reference_in_workspace() {
let mut script = fixtures();
script.push("lsp");
script.push("workspace");
let (client_connection, _recv) = initialize_language_server(
None,
serde_json::to_value(InitializeParams {
workspace_folders: Some(vec![WorkspaceFolder {
uri: path_to_uri(&script),
name: "random name".to_string(),
}]),
..Default::default()
})
.ok(),
);
script.push("baz.nu");
let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone());
let message_num = 6;
let messages =
send_reference_request(&client_connection, script.clone(), 0, 12, message_num);
assert_eq!(messages.len(), message_num);
let mut has_response = false;
for message in messages {
match message {
Message::Notification(n) => assert_eq!(n.method, "$/progress"),
Message::Response(r) => {
has_response = true;
let result = r.result.unwrap();
let array = result.as_array().unwrap();
assert!(array.contains(&serde_json::json!(
{
"uri": script.to_string().replace("baz", "bar"),
"range": { "start": { "line": 0, "character": 4 }, "end": { "line": 0, "character": 12 } }
}
)
));
assert!(array.contains(&serde_json::json!(
{
"uri": script,
"range": { "start": { "line": 6, "character": 4 }, "end": { "line": 6, "character": 12 } }
}
)
));
}
_ => panic!("unexpected message type"),
}
}
assert!(has_response);
} }
#[test] #[test]
@ -768,7 +836,7 @@ mod tests {
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let message_num = 5; let message_num = 6;
let messages = send_rename_prepare_request( let messages = send_rename_prepare_request(
&client_connection, &client_connection,
script.clone(), script.clone(),
@ -778,19 +846,24 @@ mod tests {
false, false,
); );
assert_eq!(messages.len(), message_num); assert_eq!(messages.len(), message_num);
let mut has_response = false;
for message in messages { for message in messages {
match message { match message {
Message::Notification(n) => assert_eq!(n.method, "$/progress"), Message::Notification(n) => assert_eq!(n.method, "$/progress"),
Message::Response(r) => assert_json_eq!( Message::Response(r) => {
r.result, has_response = true;
serde_json::json!({ assert_json_eq!(
"start": { "line": 6, "character": 13 }, r.result,
"end": { "line": 6, "character": 20 } serde_json::json!({
}), "start": { "line": 6, "character": 13 },
), "end": { "line": 6, "character": 20 }
}),
)
}
_ => panic!("unexpected message type"), _ => panic!("unexpected message type"),
} }
} }
assert!(has_response);
if let Message::Response(r) = send_rename_request(&client_connection, script.clone(), 6, 11) if let Message::Response(r) = send_rename_request(&client_connection, script.clone(), 6, 11)
{ {
@ -817,7 +890,7 @@ mod tests {
assert!( assert!(
changs_bar.contains( changs_bar.contains(
&serde_json::json!({ &serde_json::json!({
"range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 27 } }, "range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 29 } },
"newText": "new" "newText": "new"
}) })
)); ));
@ -847,7 +920,7 @@ mod tests {
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let message_num = 5; let message_num = 6;
let messages = send_rename_prepare_request( let messages = send_rename_prepare_request(
&client_connection, &client_connection,
script.clone(), script.clone(),
@ -857,19 +930,24 @@ mod tests {
false, false,
); );
assert_eq!(messages.len(), message_num); assert_eq!(messages.len(), message_num);
let mut has_response = false;
for message in messages { for message in messages {
match message { match message {
Message::Notification(n) => assert_eq!(n.method, "$/progress"), Message::Notification(n) => assert_eq!(n.method, "$/progress"),
Message::Response(r) => assert_json_eq!( Message::Response(r) => {
r.result, has_response = true;
serde_json::json!({ assert_json_eq!(
"start": { "line": 3, "character": 3 }, r.result,
"end": { "line": 3, "character": 8 } serde_json::json!({
}), "start": { "line": 3, "character": 3 },
), "end": { "line": 3, "character": 8 }
}),
)
}
_ => panic!("unexpected message type"), _ => panic!("unexpected message type"),
} }
} }
assert!(has_response);
if let Message::Response(r) = send_rename_request(&client_connection, script.clone(), 3, 5) if let Message::Response(r) = send_rename_request(&client_connection, script.clone(), 3, 5)
{ {
@ -934,25 +1012,29 @@ mod tests {
} else { } else {
panic!("Progress not cancelled"); panic!("Progress not cancelled");
}; };
let mut has_response = false;
for message in messages { for message in messages {
match message { match message {
Message::Notification(n) => assert_eq!(n.method, "$/progress"), Message::Notification(n) => assert_eq!(n.method, "$/progress"),
// the response of the preempting hover request // the response of the preempting hover request
Message::Response(r) => assert_json_eq!( Message::Response(r) => {
r.result, has_response = true;
serde_json::json!({ assert_json_eq!(
"contents": { r.result,
"kind": "markdown", serde_json::json!({
"value": "\n---\n### Usage \n```nu\n foo str {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n" "contents": {
} "kind": "markdown",
}), "value": "\n---\n### Usage \n```nu\n foo str {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
), }
}),
)
}
_ => panic!("unexpected message type"), _ => panic!("unexpected message type"),
} }
} }
assert!(has_response);
if let Message::Response(r) = send_rename_request(&client_connection, script.clone(), 6, 11) if let Message::Response(r) = send_rename_request(&client_connection, script, 6, 11) {
{
// should not return any changes // should not return any changes
assert_json_eq!(r.result.unwrap()["changes"], serde_json::json!({})); assert_json_eq!(r.result.unwrap()["changes"], serde_json::json!({}));
} else { } else {
@ -998,7 +1080,7 @@ mod tests {
let (client_connection, _recv) = initialize_language_server(None, None); let (client_connection, _recv) = initialize_language_server(None, None);
open_unchecked(&client_connection, script.clone()); open_unchecked(&client_connection, script.clone());
let message = send_document_highlight_request(&client_connection, script.clone(), 3, 5); let message = send_document_highlight_request(&client_connection, script, 3, 5);
let Message::Response(r) = message else { let Message::Response(r) = message else {
panic!("unexpected message type"); panic!("unexpected message type");
}; };
@ -1010,4 +1092,76 @@ mod tests {
]), ]),
); );
} }
#[test]
fn document_highlight_module_alias() {
let mut script = fixtures();
script.push("lsp");
script.push("goto");
script.push("use_module.nu");
let script = path_to_uri(&script);
let (client_connection, _recv) = initialize_language_server(None, None);
open_unchecked(&client_connection, script.clone());
let message = send_document_highlight_request(&client_connection, script.clone(), 1, 26);
let Message::Response(r) = message else {
panic!("unexpected message type");
};
assert_json_eq!(
r.result,
serde_json::json!([
{ "range": { "start": { "line": 1, "character": 25 }, "end": { "line": 1, "character": 33 } }, "kind": 1 },
{ "range": { "start": { "line": 2, "character": 30 }, "end": { "line": 2, "character": 38 } }, "kind": 1 }
]),
);
let message = send_document_highlight_request(&client_connection, script, 0, 10);
let Message::Response(r) = message else {
panic!("unexpected message type");
};
assert_json_eq!(
r.result,
serde_json::json!([
{ "range": { "start": { "line": 0, "character": 4 }, "end": { "line": 0, "character": 13 } }, "kind": 1 },
{ "range": { "start": { "line": 1, "character": 12 }, "end": { "line": 1, "character": 21 } }, "kind": 1 }
]),
);
}
#[test]
fn document_highlight_module_record() {
let mut script = fixtures();
script.push("lsp");
script.push("workspace");
script.push("baz.nu");
let script = path_to_uri(&script);
let (client_connection, _recv) = initialize_language_server(None, None);
open_unchecked(&client_connection, script.clone());
let message = send_document_highlight_request(&client_connection, script.clone(), 8, 0);
let Message::Response(r) = message else {
panic!("unexpected message type");
};
assert_json_eq!(
r.result,
serde_json::json!([
{ "range": { "start": { "line": 6, "character": 26 }, "end": { "line": 6, "character": 33 } }, "kind": 1 },
{ "range": { "start": { "line": 8, "character": 1 }, "end": { "line": 8, "character": 8 } }, "kind": 1 },
]),
);
let message = send_document_highlight_request(&client_connection, script, 10, 7);
let Message::Response(r) = message else {
panic!("unexpected message type");
};
assert_json_eq!(
r.result,
serde_json::json!([
{ "range": { "start": { "line": 10, "character": 4 }, "end": { "line": 10, "character": 12 } }, "kind": 1 },
{ "range": { "start": { "line": 11, "character": 1 }, "end": { "line": 11, "character": 8 } }, "kind": 1 },
]),
);
}
} }

View File

@ -1,4 +1,4 @@
use foo.nu [ foooo "foo str" ] use ./foo.nu [ foooo "foo str" ]
export def "bar str" [ export def "bar str" [
] { ] {

12
tests/fixtures/lsp/workspace/baz.nu vendored Normal file
View File

@ -0,0 +1,12 @@
overlay use ./foo.nu as prefix --prefix
alias aname = prefix mod name sub module cmd name
aname
prefix foo str
overlay hide prefix
use ./foo.nu [ "mod name" cst_mod ]
$cst_mod."sub module"."sub sub module".var_name
mod name sub module cmd name
let $cst_mod = 1
$cst_mod

View File

@ -5,3 +5,20 @@ export def foooo [
} }
export def "foo str" [] { "foo" } export def "foo str" [] { "foo" }
export module "mod name" {
export module "sub module" {
export def "cmd name" [] { }
}
}
# cmt
export module cst_mod {
# sub cmt
export module "sub module" {
# sub sub cmt
export module "sub sub module" {
export const var_name = "const value"
}
}
}