Fix panic with completion ranges and autoclose regions interop (#35408)

As reported [in
Discord](https://discord.com/channels/869392257814519848/1106226198494859355/1398470747227426948)
C projects with `"` as "brackets" that autoclose, may invoke panics when
edited at the end of the file.

With a single selection-caret (`ˇ`), at the end of the file,
```c
ifndef BAR_H

int fn_branch(bool do_branch1, bool do_branch2);

```
gets an LSP response from clangd
```jsonc
{
  "filterText": "AGL/",
  "insertText": "AGL/",
  "insertTextFormat": 1,
  "kind": 17,
  "label": " AGL/",
  "labelDetails": {},
  "score": 0.78725427389144897,
  "sortText": "40b67681AGL/",
  "textEdit": {
    "newText": "AGL/",
    "range": { "end": { "character": 11, "line": 8 }, "start": { "character": 10, "line": 8 } }
  }
}
```

which replaces `"` after the caret (character/column 11, 0-indexed).
This is reasonable, as regular follow-up (proposed in further
completions), is a suffix + a closing `"`:

<img width="842" height="259" alt="image"
src="https://github.com/user-attachments/assets/ea56f621-7008-4ce2-99ba-87344ddf33d2"
/>

Yet when Zed handles user input of `"`, it panics due to multiple
reasons:

* after applying any snippet text edit, Zed did a selection change:
5537987630/crates/editor/src/editor.rs (L9539-L9545)
which caused eventual autoclose region invalidation:
5537987630/crates/editor/src/editor.rs (L2970)

This covers all cases that insert the `include""` text.

* after applying any user input and "plain" text edit, Zed did not
invalidate any autoclose regions at all, relying on the "bracket" (which
includes `"`) autoclose logic to rule edge cases out

* bracket autoclose logic detects previous `"` and considers the new
user input as a valid closure, hence no autoclose region needed.
But there is an autoclose bracket data after the plaintext completion
insertion (`AGL/`) really, and it's not invalidated after `"` handling

* in addition to that, `Anchor::is_valid` method in `text` panicked, and
required `fn try_fragment_id_for_anchor` to handle "pointing at odd,
after the end of the file, offset" cases as `false`

A test reproducing the feedback and 2 fixes added: proper, autoclose
region invalidation call which required the invalidation logic tweaked a
bit, and "superficial", "do not apply bad selections that cause panics"
fix in the editor to be more robust

Release Notes:

- Fixed panic with completion ranges and autoclose regions interop

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Kirill Bulatov 2025-07-31 19:18:26 +03:00
parent ae1bf978e5
commit 17404118de
8 changed files with 317 additions and 157 deletions

169
Cargo.lock generated
View file

@ -4276,41 +4276,6 @@ dependencies = [
"workspace-hack", "workspace-hack",
] ]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.101",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.101",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.5.3" version = "5.5.3"
@ -4526,37 +4491,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.101",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.19" version = "0.99.19"
@ -4966,6 +4900,7 @@ dependencies = [
"text", "text",
"theme", "theme",
"time", "time",
"tree-sitter-c",
"tree-sitter-html", "tree-sitter-html",
"tree-sitter-python", "tree-sitter-python",
"tree-sitter-rust", "tree-sitter-rust",
@ -5926,7 +5861,7 @@ dependencies = [
"ignore", "ignore",
"libc", "libc",
"log", "log",
"notify", "notify 8.0.0",
"objc", "objc",
"parking_lot", "parking_lot",
"paths", "paths",
@ -7483,18 +7418,16 @@ dependencies = [
[[package]] [[package]]
name = "handlebars" name = "handlebars"
version = "6.3.2" version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
dependencies = [ dependencies = [
"derive_builder",
"log", "log",
"num-order",
"pest", "pest",
"pest_derive", "pest_derive",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 1.0.69",
] ]
[[package]] [[package]]
@ -8165,12 +8098,6 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.0.3"
@ -8389,6 +8316,17 @@ dependencies = [
"zeta", "zeta",
] ]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.11.0" version = "0.11.0"
@ -8542,7 +8480,7 @@ dependencies = [
"fnv", "fnv",
"lazy_static", "lazy_static",
"libc", "libc",
"mio", "mio 1.0.3",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"tempfile", "tempfile",
@ -9981,9 +9919,9 @@ dependencies = [
[[package]] [[package]]
name = "mdbook" name = "mdbook"
version = "0.4.48" version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6fbb4ac2d9fd7aa987c3510309ea3c80004a968d063c42f0d34fea070817c1" checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"anyhow", "anyhow",
@ -9993,12 +9931,11 @@ dependencies = [
"elasticlunr-rs", "elasticlunr-rs",
"env_logger 0.11.8", "env_logger 0.11.8",
"futures-util", "futures-util",
"handlebars 6.3.2", "handlebars 5.1.2",
"hex",
"ignore", "ignore",
"log", "log",
"memchr", "memchr",
"notify", "notify 6.1.1",
"notify-debouncer-mini", "notify-debouncer-mini",
"once_cell", "once_cell",
"opener", "opener",
@ -10007,7 +9944,6 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"shlex", "shlex",
"tempfile", "tempfile",
"tokio", "tokio",
@ -10150,6 +10086,18 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.3" version = "1.0.3"
@ -10519,6 +10467,25 @@ dependencies = [
"zed_actions", "zed_actions",
] ]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.9.0",
"crossbeam-channel",
"filetime",
"fsevent-sys 4.1.0",
"inotify 0.9.6",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"walkdir",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "notify" name = "notify"
version = "8.0.0" version = "8.0.0"
@ -10527,11 +10494,11 @@ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"filetime", "filetime",
"fsevent-sys 4.1.0", "fsevent-sys 4.1.0",
"inotify", "inotify 0.11.0",
"kqueue", "kqueue",
"libc", "libc",
"log", "log",
"mio", "mio 1.0.3",
"notify-types", "notify-types",
"walkdir", "walkdir",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@ -10539,14 +10506,13 @@ dependencies = [
[[package]] [[package]]
name = "notify-debouncer-mini" name = "notify-debouncer-mini"
version = "0.6.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8" checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
dependencies = [ dependencies = [
"crossbeam-channel",
"log", "log",
"notify", "notify 6.1.1",
"notify-types",
"tempfile",
] ]
[[package]] [[package]]
@ -10686,21 +10652,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-modular"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
[[package]]
name = "num-order"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
dependencies = [
"num-modular",
]
[[package]] [[package]]
name = "num-rational" name = "num-rational"
version = "0.4.2" version = "0.4.2"
@ -16549,7 +16500,7 @@ dependencies = [
"backtrace", "backtrace",
"bytes 1.10.1", "bytes 1.10.1",
"libc", "libc",
"mio", "mio 1.0.3",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
@ -19726,7 +19677,7 @@ dependencies = [
"md-5", "md-5",
"memchr", "memchr",
"miniz_oxide", "miniz_oxide",
"mio", "mio 1.0.3",
"naga", "naga",
"nix 0.29.0", "nix 0.29.0",
"nom", "nom",

View file

@ -22,6 +22,7 @@ test-support = [
"theme/test-support", "theme/test-support",
"util/test-support", "util/test-support",
"workspace/test-support", "workspace/test-support",
"tree-sitter-c",
"tree-sitter-rust", "tree-sitter-rust",
"tree-sitter-typescript", "tree-sitter-typescript",
"tree-sitter-html", "tree-sitter-html",
@ -76,6 +77,7 @@ telemetry.workspace = true
text.workspace = true text.workspace = true
time.workspace = true time.workspace = true
theme.workspace = true theme.workspace = true
tree-sitter-c = { workspace = true, optional = true }
tree-sitter-html = { workspace = true, optional = true } tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true }
@ -106,6 +108,7 @@ settings = { workspace = true, features = ["test-support"] }
tempfile.workspace = true tempfile.workspace = true
text = { workspace = true, features = ["test-support"] } text = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] }
tree-sitter-c.workspace = true
tree-sitter-html.workspace = true tree-sitter-html.workspace = true
tree-sitter-rust.workspace = true tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true tree-sitter-typescript.workspace = true

View file

@ -1305,6 +1305,7 @@ impl Default for SelectionHistoryMode {
/// ///
/// Similarly, you might want to disable scrolling if you don't want the viewport to /// Similarly, you might want to disable scrolling if you don't want the viewport to
/// move. /// move.
#[derive(Clone)]
pub struct SelectionEffects { pub struct SelectionEffects {
nav_history: Option<bool>, nav_history: Option<bool>,
completions: bool, completions: bool,
@ -2944,10 +2945,12 @@ impl Editor {
} }
} }
let selection_anchors = self.selections.disjoint_anchors();
if self.focus_handle.is_focused(window) && self.leader_id.is_none() { if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selections( buffer.set_active_selections(
&self.selections.disjoint_anchors(), &selection_anchors,
self.selections.line_mode, self.selections.line_mode,
self.cursor_shape, self.cursor_shape,
cx, cx,
@ -2964,9 +2967,8 @@ impl Editor {
self.select_next_state = None; self.select_next_state = None;
self.select_prev_state = None; self.select_prev_state = None;
self.select_syntax_node_history.try_clear(); self.select_syntax_node_history.try_clear();
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.invalidate_autoclose_regions(&selection_anchors, buffer);
self.snippet_stack self.snippet_stack.invalidate(&selection_anchors, buffer);
.invalidate(&self.selections.disjoint_anchors(), buffer);
self.take_rename(false, window, cx); self.take_rename(false, window, cx);
let newest_selection = self.selections.newest_anchor(); let newest_selection = self.selections.newest_anchor();
@ -4047,7 +4049,8 @@ impl Editor {
// then don't insert that closing bracket again; just move the selection // then don't insert that closing bracket again; just move the selection
// past the closing bracket. // past the closing bracket.
let should_skip = selection.end == region.range.end.to_point(&snapshot) let should_skip = selection.end == region.range.end.to_point(&snapshot)
&& text.as_ref() == region.pair.end.as_str(); && text.as_ref() == region.pair.end.as_str()
&& snapshot.contains_str_at(region.range.end, text.as_ref());
if should_skip { if should_skip {
let anchor = snapshot.anchor_after(selection.end); let anchor = snapshot.anchor_after(selection.end);
new_selections new_selections
@ -4973,13 +4976,17 @@ impl Editor {
}) })
} }
/// Remove any autoclose regions that no longer contain their selection. /// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges.
fn invalidate_autoclose_regions( fn invalidate_autoclose_regions(
&mut self, &mut self,
mut selections: &[Selection<Anchor>], mut selections: &[Selection<Anchor>],
buffer: &MultiBufferSnapshot, buffer: &MultiBufferSnapshot,
) { ) {
self.autoclose_regions.retain(|state| { self.autoclose_regions.retain(|state| {
if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) {
return false;
}
let mut i = 0; let mut i = 0;
while let Some(selection) = selections.get(i) { while let Some(selection) = selections.get(i) {
if selection.end.cmp(&state.range.start, buffer).is_lt() { if selection.end.cmp(&state.range.start, buffer).is_lt() {
@ -5891,18 +5898,20 @@ impl Editor {
text: new_text[common_prefix_len..].into(), text: new_text[common_prefix_len..].into(),
}); });
self.transact(window, cx, |this, window, cx| { self.transact(window, cx, |editor, window, cx| {
if let Some(mut snippet) = snippet { if let Some(mut snippet) = snippet {
snippet.text = new_text.to_string(); snippet.text = new_text.to_string();
this.insert_snippet(&ranges, snippet, window, cx).log_err(); editor
.insert_snippet(&ranges, snippet, window, cx)
.log_err();
} else { } else {
this.buffer.update(cx, |buffer, cx| { editor.buffer.update(cx, |multi_buffer, cx| {
let auto_indent = match completion.insert_text_mode { let auto_indent = match completion.insert_text_mode {
Some(InsertTextMode::AS_IS) => None, Some(InsertTextMode::AS_IS) => None,
_ => this.autoindent_mode.clone(), _ => editor.autoindent_mode.clone(),
}; };
let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
buffer.edit(edits, auto_indent, cx); multi_buffer.edit(edits, auto_indent, cx);
}); });
} }
for (buffer, edits) in linked_edits { for (buffer, edits) in linked_edits {
@ -5921,8 +5930,9 @@ impl Editor {
}) })
} }
this.refresh_inline_completion(true, false, window, cx); editor.refresh_inline_completion(true, false, window, cx);
}); });
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot);
let show_new_completions_on_confirm = completion let show_new_completions_on_confirm = completion
.confirm .confirm
@ -9562,27 +9572,46 @@ impl Editor {
// Check whether the just-entered snippet ends with an auto-closable bracket. // Check whether the just-entered snippet ends with an auto-closable bracket.
if self.autoclose_regions.is_empty() { if self.autoclose_regions.is_empty() {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
for selection in &mut self.selections.all::<Point>(cx) { let mut all_selections = self.selections.all::<Point>(cx);
for selection in &mut all_selections {
let selection_head = selection.head(); let selection_head = selection.head();
let Some(scope) = snapshot.language_scope_at(selection_head) else { let Some(scope) = snapshot.language_scope_at(selection_head) else {
continue; continue;
}; };
let mut bracket_pair = None; let mut bracket_pair = None;
let next_chars = snapshot.chars_at(selection_head).collect::<String>(); let max_lookup_length = scope
let prev_chars = snapshot .brackets()
.reversed_chars_at(selection_head) .map(|(pair, _)| {
.collect::<String>(); pair.start
for (pair, enabled) in scope.brackets() { .as_str()
if enabled .chars()
&& pair.close .count()
&& prev_chars.starts_with(pair.start.as_str()) .max(pair.end.as_str().chars().count())
&& next_chars.starts_with(pair.end.as_str()) })
{ .max();
bracket_pair = Some(pair.clone()); if let Some(max_lookup_length) = max_lookup_length {
break; let next_text = snapshot
.chars_at(selection_head)
.take(max_lookup_length)
.collect::<String>();
let prev_text = snapshot
.reversed_chars_at(selection_head)
.take(max_lookup_length)
.collect::<String>();
for (pair, enabled) in scope.brackets() {
if enabled
&& pair.close
&& prev_text.starts_with(pair.start.as_str())
&& next_text.starts_with(pair.end.as_str())
{
bracket_pair = Some(pair.clone());
break;
}
} }
} }
if let Some(pair) = bracket_pair { if let Some(pair) = bracket_pair {
let snapshot_settings = snapshot.language_settings_at(selection_head, cx); let snapshot_settings = snapshot.language_settings_at(selection_head, cx);
let autoclose_enabled = let autoclose_enabled =

View file

@ -13356,6 +13356,178 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
cx.assert_editor_state("fn a() {}\n unsafeˇ"); cx.assert_editor_state("fn a() {}\n unsafeˇ");
} }
#[gpui::test]
async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let language =
Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
let mut cx = EditorLspTestContext::new(
language,
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
..lsp::CompletionOptions::default()
}),
..lsp::ServerCapabilities::default()
},
cx,
)
.await;
cx.set_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
ˇ",
);
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("#", window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("i", window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("n", window, cx);
});
cx.executor().run_until_parked();
cx.assert_editor_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#inˇ",
);
cx.lsp
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
item_defaults: None,
items: vec![lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::SNIPPET),
label_details: Some(lsp::CompletionItemLabelDetails {
detail: Some("header".to_string()),
description: None,
}),
label: " include".to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 8,
character: 1,
},
end: lsp::Position {
line: 8,
character: 1,
},
},
new_text: "include \"$0\"".to_string(),
})),
sort_text: Some("40b67681include".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
filter_text: Some("include".to_string()),
insert_text: Some("include \"$0\"".to_string()),
..lsp::CompletionItem::default()
}],
})))
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
});
cx.executor().run_until_parked();
cx.assert_editor_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include \"ˇ\"",
);
cx.lsp
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: true,
item_defaults: None,
items: vec![lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FILE),
label: "AGL/".to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 8,
character: 10,
},
end: lsp::Position {
line: 8,
character: 11,
},
},
new_text: "AGL/".to_string(),
})),
sort_text: Some("40b67681AGL/".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
filter_text: Some("AGL/".to_string()),
insert_text: Some("AGL/".to_string()),
..lsp::CompletionItem::default()
}],
})))
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
});
cx.executor().run_until_parked();
cx.assert_editor_state(
r##"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include "AGL/ˇ"##,
);
cx.update_editor(|editor, window, cx| {
editor.handle_input("\"", window, cx);
});
cx.executor().run_until_parked();
cx.assert_editor_state(
r##"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include "AGL/"ˇ"##,
);
}
#[gpui::test] #[gpui::test]
async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) { async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});

View file

@ -167,10 +167,10 @@ impl Anchor {
if *self == Anchor::min() || *self == Anchor::max() { if *self == Anchor::min() || *self == Anchor::max() {
true true
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
excerpt.contains(self) (self.text_anchor == excerpt.range.context.start
&& (self.text_anchor == excerpt.range.context.start || self.text_anchor == excerpt.range.context.end
|| self.text_anchor == excerpt.range.context.end || self.text_anchor.is_valid(&excerpt.buffer))
|| self.text_anchor.is_valid(&excerpt.buffer)) && excerpt.contains(self)
} else { } else {
false false
} }

View file

@ -2269,7 +2269,7 @@ impl LspCommand for GetCompletions {
// the range based on the syntax tree. // the range based on the syntax tree.
None => { None => {
if self.position != clipped_position { if self.position != clipped_position {
log::info!("completion out of expected range"); log::info!("completion out of expected range ");
return false; return false;
} }
@ -2483,7 +2483,9 @@ pub(crate) fn parse_completion_text_edit(
let start = snapshot.clip_point_utf16(range.start, Bias::Left); let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left); let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 { if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range"); log::info!(
"completion out of expected range, start: {start:?}, end: {end:?}, range: {range:?}"
);
return None; return None;
} }
snapshot.anchor_before(start)..snapshot.anchor_after(end) snapshot.anchor_before(start)..snapshot.anchor_after(end)

View file

@ -99,7 +99,9 @@ impl Anchor {
} else if self.buffer_id != Some(buffer.remote_id) { } else if self.buffer_id != Some(buffer.remote_id) {
false false
} else { } else {
let fragment_id = buffer.fragment_id_for_anchor(self); let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else {
return false;
};
let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None); let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None);
fragment_cursor.seek(&Some(fragment_id), Bias::Left); fragment_cursor.seek(&Some(fragment_id), Bias::Left);
fragment_cursor fragment_cursor

View file

@ -2330,10 +2330,19 @@ impl BufferSnapshot {
} }
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
self.try_fragment_id_for_anchor(anchor).unwrap_or_else(|| {
panic!(
"invalid anchor {:?}. buffer id: {}, version: {:?}",
anchor, self.remote_id, self.version,
)
})
}
fn try_fragment_id_for_anchor(&self, anchor: &Anchor) -> Option<&Locator> {
if *anchor == Anchor::MIN { if *anchor == Anchor::MIN {
Locator::min_ref() Some(Locator::min_ref())
} else if *anchor == Anchor::MAX { } else if *anchor == Anchor::MAX {
Locator::max_ref() Some(Locator::max_ref())
} else { } else {
let anchor_key = InsertionFragmentKey { let anchor_key = InsertionFragmentKey {
timestamp: anchor.timestamp, timestamp: anchor.timestamp,
@ -2354,20 +2363,12 @@ impl BufferSnapshot {
insertion_cursor.prev(); insertion_cursor.prev();
} }
let Some(insertion) = insertion_cursor.item().filter(|insertion| { insertion_cursor
if cfg!(debug_assertions) { .item()
insertion.timestamp == anchor.timestamp .filter(|insertion| {
} else { !cfg!(debug_assertions) || insertion.timestamp == anchor.timestamp
true })
} .map(|insertion| &insertion.fragment_id)
}) else {
panic!(
"invalid anchor {:?}. buffer id: {}, version: {:?}",
anchor, self.remote_id, self.version
);
};
&insertion.fragment_id
} }
} }