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
#define BAR_H

#include <stdbool.h>

int fn_branch(bool do_branch1, bool do_branch2);

#endif // BAR_H
#include"ˇ"
```
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 committed by GitHub
parent a2aea00253
commit 6a8be1714e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 258 additions and 48 deletions

View file

@ -99,7 +99,9 @@ impl Anchor {
} else if self.buffer_id != Some(buffer.remote_id) {
false
} 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);
fragment_cursor.seek(&Some(fragment_id), Bias::Left);
fragment_cursor

View file

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