editor: Fix panic in text_layout_details pre layout (#55816)

Fixes a panic at `Editor::text_layout_details` when called against an
editor whose element has never been laid out (i.e., `set_style` has
never been called, so the cached `style` is still `None`).

We've seen this crash once through a helix motion. The exact production
sequence isn't clear — for the editor to receive a vim action without
ever having been drawn, the active item would have to have changed
inside the same update tick that ends with the deferred `search_submit`,
which is narrow but not impossible (since it's dispatched by the
workspace, not the editor).

Release Notes:

- Fixed a rare panic when invoking helix motions on an editor that had
not yet been laid out.
This commit is contained in:
Agus Zubiaga authored and GitHub committed 2026-05-05 21:46:22 +00:00
1 parent 26b9e07fa8
commit de765f98bb
2 files changed
+56 -1

No files matched your search

+1 -1
View File
@@ -5985,7 +5985,7 @@ impl Editor {
pub fn text_layout_details(&self, window: &mut Window, cx: &mut App) -> TextLayoutDetails {
TextLayoutDetails {
text_system: window.text_system().clone(),
editor_style: self.style.clone().unwrap(),
editor_style: self.style.clone().unwrap_or_else(|| self.create_style(cx)),
rem_size: window.rem_size(),
scroll_anchor: self.scroll_manager.shared_scroll_anchor(cx),
visible_rows: self.visible_line_count(),
+55
View File
@@ -2960,6 +2960,61 @@ mod test {
cx.assert_state("«ˇone» two three", Mode::HelixSelect);
}
// Regression test for ZED-758: helix motions called
// `Editor::text_layout_details` on an editor whose `style` had never
// been set, panicking on `unwrap()`.
#[gpui::test]
async fn test_helix_motion_on_unrendered_editor(cx: &mut gpui::TestAppContext) {
use editor::{Editor, EditorMode, SelectionEffects};
use multi_buffer::{MultiBuffer, MultiBufferOffset};
VimTestContext::init(cx);
cx.update(|cx| {
VimTestContext::init_keybindings(true, cx);
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |s| {
s.vim_mode = Some(true);
s.helix_mode = Some(true);
});
});
});
let cx = cx.add_empty_window();
let editor = cx.update(|window, cx| {
use gpui::AppContext as _;
let buffer = MultiBuffer::build_simple("one two three", cx);
cx.new(|cx| {
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
});
editor
})
});
let vim = editor
.read_with(cx, |editor, _| editor.addon::<VimAddon>().cloned())
.expect("VimAddon should be auto-attached to new editors when vim mode is enabled");
cx.update(|window, cx| {
vim.entity.update(cx, |vim, cx| {
vim.switch_mode(Mode::HelixNormal, true, window, cx);
vim.helix_move_and_collapse(crate::motion::Motion::Left, None, window, cx);
});
});
let cursor_offset = cx.update(|_, cx| {
editor.update(cx, |editor, cx| {
editor
.selections
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
.head()
})
});
assert_eq!(cursor_offset, MultiBufferOffset(3));
}
#[gpui::test]
async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;