feat(completion): stdlib virtual path completion & exportable completion (#15270)

# Description

More completions for `use` command.

~Also optimizes the span fix of #15238 to allow changing the text after
the cursor.~

# User-Facing Changes

<img width="299" alt="image"
src="https://github.com/user-attachments/assets/a5c45f46-40e4-4c50-9408-7b147ed11dc4"
/>

<img width="383" alt="image"
src="https://github.com/user-attachments/assets/fbeec173-511e-4c72-8995-bc1caa3ef0d3"
/>


# Tests + Formatting

+3

# After Submitting
This commit is contained in:
zc he
2025-04-01 20:13:07 +08:00
committed by GitHub
parent 1dcaffb792
commit 6c0b65b570
13 changed files with 470 additions and 48 deletions

View File

@ -28,22 +28,15 @@ impl LanguageServer {
.and_then(|s| s.chars().next())
.is_some_and(|c| c.is_whitespace() || "|(){}[]<>,:;".contains(c));
let (results, engine_state) = if need_fallback {
let engine_state = Arc::new(self.initial_engine_state.clone());
let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new()));
(
completer.fetch_completions_at(&file_text[..location], location),
engine_state,
)
self.need_parse |= need_fallback;
let engine_state = Arc::new(self.new_engine_state());
let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new()));
let results = if need_fallback {
completer.fetch_completions_at(&file_text[..location], location)
} else {
let engine_state = Arc::new(self.new_engine_state());
let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new()));
let file_path = uri_to_path(&path_uri);
let filename = file_path.to_str()?;
(
completer.fetch_completions_within_file(filename, location, &file_text),
engine_state,
)
completer.fetch_completions_within_file(filename, location, &file_text)
};
let docs = self.docs.lock().ok()?;
@ -63,10 +56,8 @@ impl LanguageServer {
}
let span = r.suggestion.span;
let range = span_to_range(&Span::new(span.start, span.end), file, 0);
let text_edit = Some(CompletionTextEdit::Edit(TextEdit {
range,
range: span_to_range(&Span::new(span.start, span.end), file, 0),
new_text: label_value.clone(),
}));
@ -236,7 +227,7 @@ mod tests {
"detail": "Edit nu configurations.",
"textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
"newText": "config nu "
}
},
},
])
);
@ -549,4 +540,96 @@ mod tests {
])
);
}
#[test]
fn complete_use_arguments() {
let (client_connection, _recv) = initialize_language_server(None, None);
let mut script = fixtures();
script.push("lsp");
script.push("completion");
script.push("use.nu");
let script = path_to_uri(&script);
open_unchecked(&client_connection, script.clone());
let resp = send_complete_request(&client_connection, script.clone(), 4, 17);
assert_json_include!(
actual: result_from_message(resp),
expected: serde_json::json!([
{
"label": "std-rfc",
"labelDetails": { "description": "module" },
"textEdit": {
"newText": "std-rfc",
"range": { "start": { "character": 11, "line": 4 }, "end": { "character": 17, "line": 4 } }
},
"kind": 9 // module kind
}
])
);
let resp = send_complete_request(&client_connection, script.clone(), 5, 22);
assert_json_include!(
actual: result_from_message(resp),
expected: serde_json::json!([
{
"label": "clip",
"labelDetails": { "description": "module" },
"textEdit": {
"newText": "clip",
"range": { "start": { "character": 19, "line": 5 }, "end": { "character": 23, "line": 5 } }
},
"kind": 9 // module kind
}
])
);
let resp = send_complete_request(&client_connection, script.clone(), 5, 35);
assert_json_include!(
actual: result_from_message(resp),
expected: serde_json::json!([
{
"label": "paste",
"labelDetails": { "description": "custom" },
"textEdit": {
"newText": "paste",
"range": { "start": { "character": 32, "line": 5 }, "end": { "character": 37, "line": 5 } }
},
"kind": 2
}
])
);
let resp = send_complete_request(&client_connection, script.clone(), 6, 14);
assert_json_include!(
actual: result_from_message(resp),
expected: serde_json::json!([
{
"label": "null_device",
"labelDetails": { "description": "variable" },
"textEdit": {
"newText": "null_device",
"range": { "start": { "character": 8, "line": 6 }, "end": { "character": 14, "line": 6 } }
},
"kind": 6 // variable kind
}
])
);
let resp = send_complete_request(&client_connection, script, 7, 13);
assert_json_include!(
actual: result_from_message(resp),
expected: serde_json::json!([
{
"label": "foo",
"labelDetails": { "description": "variable" },
"textEdit": {
"newText": "foo",
"range": { "start": { "character": 11, "line": 7 }, "end": { "character": 14, "line": 7 } }
},
"kind": 6 // variable kind
}
])
);
}
}

View File

@ -440,6 +440,7 @@ mod tests {
TextDocumentPositionParams, WorkDoneProgressParams,
};
use nu_protocol::{debugger::WithoutDebug, engine::Stack, PipelineData, ShellError, Value};
use nu_std::load_standard_library;
use std::sync::mpsc::{self, Receiver};
use std::time::Duration;
@ -455,6 +456,7 @@ mod tests {
let engine_state = nu_cmd_lang::create_default_context();
let mut engine_state = nu_command::add_shell_command_context(engine_state);
engine_state.generate_nu_constant();
assert!(load_standard_library(&mut engine_state).is_ok());
let cwd = std::env::current_dir().expect("Could not get current working directory.");
engine_state.add_env_var(
"PWD".into(),