fix(lsp): several edge cases of inaccurate references

This commit is contained in:
blindfs 2025-04-08 11:08:06 +08:00
parent 15146e68ad
commit dc8b50a21e
9 changed files with 247 additions and 81 deletions

View File

@ -7,15 +7,20 @@ use nu_protocol::{
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) -> (Vec<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!")
.to_vec(),
Span::new(span.start.saturating_add(1), span.end.saturating_sub(1)),
)
} else { } else {
span (text.to_vec(), span)
} }
} }
@ -70,9 +75,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
@ -114,8 +118,8 @@ fn try_find_id_in_mod(
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())?;
let found_id = Id::Module(module_id); let found_id = Id::Module(module_id, name.as_bytes().to_vec());
let found_span = strip_quotes(arg.span(), working_set); 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))
@ -160,14 +164,16 @@ fn try_find_id_in_use(
(*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 == name)
.then_some(Id::Module(*module_id, name.to_vec()))
})
}
None => module None => module
.submodules .submodules
.get(name) .get(name)
.cloned() .map(|id| Id::Module(*id, name.to_vec()))
.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).cloned().map(Id::Variable)),
_ => None, _ => None,
@ -178,16 +184,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 {
// 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.to_vec()), 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.to_vec()), clean_span));
} }
} }
@ -200,14 +207,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 +222,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) => {
@ -248,7 +254,7 @@ fn try_find_id_in_overlay(
id: Option<&Id>, id: Option<&Id>,
) -> Option<(Id, Span)> { ) -> Option<(Id, Span)> {
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 +262,22 @@ 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().to_vec());
id.is_none_or(|id_r| found_id == *id_r) id.is_none_or(|id_r| found_id == *id_r)
.then_some((found_id, strip_quotes(span, working_set))) .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(
working_set.find_overlay(name.as_bytes())?.origin,
name.as_bytes().to_vec(),
);
id.is_none_or(|id_r| found_id == *id_r) id.is_none_or(|id_r| found_id == *id_r)
.then_some((found_id, strip_quotes(span, working_set))) .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 +285,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,
}, },
@ -349,7 +359,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, vec![]), span))
}
// terminal value expressions // terminal value expressions
Expr::Bool(_) Expr::Bool(_)
| Expr::Binary(_) | Expr::Binary(_)

View File

@ -55,7 +55,7 @@ impl LanguageServer {
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
} }

View File

@ -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()

View File

@ -43,7 +43,7 @@ pub(crate) enum Id {
Variable(VarId), Variable(VarId),
Declaration(DeclId), Declaration(DeclId),
Value(Type), Value(Type),
Module(ModuleId), Module(ModuleId, Vec<u8>),
CellPath(VarId, Vec<PathMember>), CellPath(VarId, Vec<PathMember>),
External(String), External(String),
} }

View File

@ -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) {
@ -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), vec![]),
doc, doc,
&cached_file.covered_span, &cached_file.covered_span,
) )

View File

@ -101,9 +101,14 @@ impl LanguageServer {
let (id, cursor_span) = find_id(&block, &working_set, &location)?; let (id, cursor_span) = find_id(&block, &working_set, &location)?;
let mut refs = find_reference_by_id(&block, &working_set, &id); let mut refs = find_reference_by_id(&block, &working_set, &id);
let definition_span = Self::find_definition_span_by_id(&working_set, &id); let definition_span = Self::find_definition_span_by_id(&working_set, &id);
if let Some(extra_span) = if let Some(extra_span) = Self::reference_not_in_ast(
Self::reference_not_in_ast(&id, &working_set, definition_span, file_span, cursor_span) &id,
{ &String::from_utf8_lossy(working_set.get_span_contents(cursor_span)),
&working_set,
definition_span,
file_span,
cursor_span,
) {
if !refs.contains(&extra_span) { if !refs.contains(&extra_span) {
refs.push(extra_span); refs.push(extra_span);
} }
@ -333,6 +338,7 @@ impl LanguageServer {
/// which is not covered in the AST /// which is not covered in the AST
fn reference_not_in_ast( fn reference_not_in_ast(
id: &Id, id: &Id,
name_ref: &str,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
definition_span: Option<Span>, definition_span: Option<Span>,
file_span: Span, file_span: Span,
@ -340,14 +346,17 @@ impl LanguageServer {
) -> Option<Span> { ) -> Option<Span> {
if let (Id::Variable(_), Some(decl_span)) = (&id, definition_span) { if let (Id::Variable(_), 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..)
.is_some_and(|name| name.starts_with(name_ref.as_bytes()))
.then_some(Span {
start, start,
end: start + sample_span.end - sample_span.start, end: start + sample_span.end - sample_span.start,
}); });
@ -436,6 +445,7 @@ impl LanguageServer {
.unwrap_or(Span::unknown()); .unwrap_or(Span::unknown());
if let Some(extra_span) = Self::reference_not_in_ast( if let Some(extra_span) = Self::reference_not_in_ast(
&id_tracker.id, &id_tracker.id,
&id_tracker.name,
&working_set, &working_set,
definition_span, definition_span,
file_span, file_span,
@ -635,7 +645,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 +669,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!(
@ -687,6 +699,7 @@ mod tests {
_ => panic!("unexpected message type"), _ => panic!("unexpected message type"),
} }
} }
assert!(has_response);
} }
#[test] #[test]
@ -717,14 +730,16 @@ 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!(
@ -745,6 +760,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.to_string(),
"range": { "start": { "line": 6, "character": 4 }, "end": { "line": 6, "character": 12 } }
}
)
));
}
_ => panic!("unexpected message type"),
}
}
assert!(has_response);
} }
#[test] #[test]
@ -768,7 +838,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 +848,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) => {
has_response = true;
assert_json_eq!(
r.result, r.result,
serde_json::json!({ serde_json::json!({
"start": { "line": 6, "character": 13 }, "start": { "line": 6, "character": 13 },
"end": { "line": 6, "character": 20 } "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 +892,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 +922,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 +932,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) => {
has_response = true;
assert_json_eq!(
r.result, r.result,
serde_json::json!({ serde_json::json!({
"start": { "line": 3, "character": 3 }, "start": { "line": 3, "character": 3 },
"end": { "line": 3, "character": 8 } "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,11 +1014,14 @@ 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) => {
has_response = true;
assert_json_eq!(
r.result, r.result,
serde_json::json!({ serde_json::json!({
"contents": { "contents": {
@ -946,13 +1029,14 @@ mod tests {
"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" "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 +1082,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 +1094,52 @@ mod tests {
]), ]),
); );
} }
#[test]
fn document_highlight_module_alias() {
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, 4, 17);
let Message::Response(r) = message else {
panic!("unexpected message type");
};
assert_json_eq!(
r.result,
serde_json::json!([
{ "range": { "start": { "line": 0, "character": 24 }, "end": { "line": 0, "character": 30 } }, "kind": 1 },
{ "range": { "start": { "line": 4, "character": 13 }, "end": { "line": 4, "character": 19 } }, "kind": 1 }
]),
);
}
/// TODO: associate the module record with the submodule name in `use`?
#[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, 8, 0);
let Message::Response(r) = message else {
panic!("unexpected message type");
};
assert_json_eq!(
r.result,
serde_json::json!([
{ "range": { "start": { "line": 8, "character": 1 }, "end": { "line": 8, "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" [
] { ] {

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

@ -0,0 +1,10 @@
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".var_name
mod name sub module cmd name

View File

@ -5,3 +5,15 @@ 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" [] { }
}
}
export module cst_mod {
export module "sub module" {
export const var_name = "const value"
}
}