fix(lsp): a panic caused by completion with decl_id out of range (#15576)

Fixes a bug caused by #15536 
Sorry about that, @fdncred 

# Description

I've made the panic reproducible in the test case.

TLDR: completer will sometimes return new decl_ids outside of the range
of the engine_state passed in.

# User-Facing Changes

bug fix

# Tests + Formatting

+1

# After Submitting
This commit is contained in:
zc he 2025-04-16 19:43:21 +08:00 committed by GitHub
parent 24cc2f9d87
commit cd4560e97a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 66 additions and 44 deletions

View File

@ -69,7 +69,8 @@ impl Completer for ExportableCompletion<'_> {
wrapped_name(name), wrapped_name(name),
Some(cmd.description().to_string()), Some(cmd.description().to_string()),
None, None,
SuggestionKind::Command(cmd.command_type(), Some(*decl_id)), // `None` here avoids arguments being expanded by snippet edit style for lsp
SuggestionKind::Command(cmd.command_type(), None),
); );
} }
} }

View File

@ -68,13 +68,19 @@ impl LanguageServer {
let mut idx = 0; let mut idx = 0;
// use snippet as `insert_text_format` for command argument completion // use snippet as `insert_text_format` for command argument completion
if let Some(SuggestionKind::Command(_, Some(decl_id))) = suggestion.kind { if let Some(SuggestionKind::Command(_, Some(decl_id))) = suggestion.kind {
// NOTE: for new commands defined in current context,
// which are not present in the engine state, skip the documentation and snippet.
if engine_state.num_decls() > decl_id.get() {
let cmd = engine_state.get_decl(decl_id); let cmd = engine_state.get_decl(decl_id);
doc_string = Some(Self::get_decl_description(cmd, true)); doc_string = Some(Self::get_decl_description(cmd, true));
insert_text_format = Some(InsertTextFormat::SNIPPET); insert_text_format = Some(InsertTextFormat::SNIPPET);
let signature = cmd.signature(); let signature = cmd.signature();
// add curly brackets around block arguments // add curly brackets around block arguments
// and keywords, e.g. `=` in `alias foo = bar` // and keywords, e.g. `=` in `alias foo = bar`
let mut arg_wrapper = |arg: &PositionalArg, text: String, optional: bool| -> String { let mut arg_wrapper = |arg: &PositionalArg,
text: String,
optional: bool|
-> String {
idx += 1; idx += 1;
match &arg.shape { match &arg.shape {
SyntaxShape::Block | SyntaxShape::MatchBlock => { SyntaxShape::Block | SyntaxShape::MatchBlock => {
@ -108,14 +114,16 @@ impl LanguageServer {
} }
for optional in signature.optional_positional { for optional in signature.optional_positional {
snippet_text.push(' '); snippet_text.push(' ');
snippet_text snippet_text.push_str(
.push_str(arg_wrapper(&optional, format!("{}?", optional.name), true).as_str()); arg_wrapper(&optional, format!("{}?", optional.name), true).as_str(),
);
} }
if let Some(rest) = signature.rest_positional { if let Some(rest) = signature.rest_positional {
idx += 1; idx += 1;
snippet_text.push_str(format!(" ${{{}:...{}}}", idx, rest.name).as_str()); snippet_text.push_str(format!(" ${{{}:...{}}}", idx, rest.name).as_str());
} }
} }
}
// no extra space for a command with args expanded in the snippet // no extra space for a command with args expanded in the snippet
if idx == 0 && suggestion.suggestion.append_whitespace { if idx == 0 && suggestion.suggestion.append_whitespace {
snippet_text.push(' '); snippet_text.push(' ');
@ -329,6 +337,17 @@ mod tests {
}) })
)); ));
// fallback completion on a newly defined command,
// the decl_id is missing in the engine state, this shouldn't cause any panic.
let resp = send_complete_request(&client_connection, script.clone(), 13, 9);
assert_json_include!(
actual: result_from_message(resp),
expected: serde_json::json!([
// defined before the cursor
{ "label": "config n foo bar", "detail": detail_str, "kind": 2 },
])
);
// inside parenthesis // inside parenthesis
let resp = send_complete_request(&client_connection, script, 10, 34); let resp = send_complete_request(&client_connection, script, 10, 34);
assert!(result_from_message(resp).as_array().unwrap().contains( assert!(result_from_message(resp).as_array().unwrap().contains(

View File

@ -10,3 +10,5 @@ def "config n foo bar" [
echo "🤔🐘" echo "🤔🐘"
| str substring (str substring -) | str substring (str substring -)
} }
config n # don't panic!