search: Fix toggle soft wrap not working while the search bar is focused (#61150)

Closes #34698

The buffer search query input is itself an editor, so dispatching
`editor::ToggleSoftWrap` from the command palette while the search bar
had focus toggled soft wrap on the single-line query editor (with no
visible effect) and never reached the searched editor.

This intercepts the action on the search bar in the capture phase and
applies it to the active searchable item, the same way `ToggleFoldAll`
is already relayed. Focus stays in the search bar, so you can keep
typing your query. `Editor::toggle_soft_wrap` and
`Editor::soft_wrap_mode` become `pub` (matching
`fold_all`/`has_any_buffer_folded`, which the existing `ToggleFoldAll`
relay already uses).

Includes a regression test that fails without the fix: it deploys the
search bar, dispatches `ToggleSoftWrap` with the query editor focused,
and asserts the searched editor's wrap mode changes while focus remains
in the search bar.

Release Notes:

- Fixed `editor: toggle soft wrap` doing nothing when invoked while the
buffer search bar was focused

---------

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
JaceandChristopher Biscardi authored and GitHub committed 2026-07-21 06:14:52 +00:00
1 parent 34bfb3842d
commit c5e0fb8ffd
3 files changed
+87 -10

No files matched your search

+2 -7
View File
@@ -257,7 +257,7 @@ impl Editor {
wrap_guides
}
pub(super) fn soft_wrap_mode(&self, cx: &App) -> SoftWrap {
pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap {
let settings = self.buffer.read(cx).language_settings(cx);
let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap);
match mode {
@@ -286,12 +286,7 @@ impl Editor {
}
}
pub(super) fn toggle_soft_wrap(
&mut self,
_: &ToggleSoftWrap,
_: &mut Window,
cx: &mut Context<Self>,
) {
pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, _: &mut Window, cx: &mut Context<Self>) {
if self.soft_wrap_mode_override.is_some() {
self.soft_wrap_mode_override.take();
} else {
+1 -1
View File
@@ -1158,7 +1158,7 @@ impl SplittableEditor {
}
}
fn toggle_soft_wrap(
pub fn toggle_soft_wrap(
&mut self,
_: &ToggleSoftWrap,
window: &mut Window,
+84 -2
View File
@@ -15,7 +15,7 @@ use any_vec::AnyVec;
use collections::HashMap;
use editor::{
DiffStyleControls, Editor, EditorSettings, MultiBufferOffset, SplittableEditor,
actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll},
actions::{Backtab, FoldAll, Tab, ToggleFoldAll, ToggleSoftWrap, UnfoldAll},
scroll::Autoscroll,
};
use futures::channel::oneshot;
@@ -462,6 +462,7 @@ impl Render for BufferSearchBar {
.capture_action(cx.listener(Self::tab))
.capture_action(cx.listener(Self::backtab))
.capture_action(cx.listener(Self::toggle_fold_all))
.capture_action(cx.listener(Self::toggle_soft_wrap))
.on_action(cx.listener(Self::previous_history_query))
.on_action(cx.listener(Self::next_history_query))
.on_action(cx.listener(Self::dismiss))
@@ -995,6 +996,38 @@ impl BufferSearchBar {
self.toggle_fold_all_in_item(window, cx);
}
// The query editor is an editor itself and would otherwise swallow this
// action without any visible effect, so relay it to the searched editor
// while keeping the focus in the search bar.
fn toggle_soft_wrap(
&mut self,
action: &ToggleSoftWrap,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(item) = &self.active_searchable_item else {
return;
};
// A split diff editor picks which side to toggle and keeps both sides
// in sync, while its `act_as_type(Editor)` only exposes the RHS
// editor, so relay to the split editor itself when it is split.
if let Some(split_editor) = item.act_as_type(TypeId::of::<SplittableEditor>(), cx) {
let split_editor = split_editor
.downcast::<SplittableEditor>()
.expect("Is a splittable editor");
if split_editor.read(cx).is_split() {
split_editor.update(cx, |split_editor, cx| {
split_editor.toggle_soft_wrap(action, window, cx)
});
return;
}
}
if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
let editor = item.downcast::<Editor>().expect("Is an editor");
editor.update(cx, |editor, cx| editor.toggle_soft_wrap(action, window, cx));
}
}
fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(item) = &self.active_searchable_item {
if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
@@ -1846,7 +1879,7 @@ mod tests {
use super::*;
use editor::{
DisplayPoint, Editor, HighlightKey, MultiBuffer, PathKey,
SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT, SearchSettings, SelectionEffects,
SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT, SearchSettings, SelectionEffects, SoftWrap,
display_map::DisplayRow, test::editor_test_context::EditorTestContext,
};
use futures::stream::StreamExt as _;
@@ -3188,6 +3221,55 @@ mod tests {
});
}
#[gpui::test]
async fn test_toggle_soft_wrap_relays_to_searched_editor(cx: &mut TestAppContext) {
init_globals(cx);
let (editor, search_bar, cx) = init_test(cx);
search_bar.update_in(cx, |search_bar, window, cx| {
search_bar.deploy(
&Deploy {
focus: true,
replace_enabled: false,
selection_search_enabled: false,
},
None,
window,
cx,
);
});
cx.run_until_parked();
search_bar.update_in(cx, |search_bar, window, cx| {
assert!(
search_bar.query_editor.focus_handle(cx).is_focused(window),
"query editor should be focused after deploying the search bar",
);
});
editor.update_in(cx, |editor, _, cx| {
assert!(
matches!(editor.soft_wrap_mode(cx), SoftWrap::None),
"soft wrap should be disabled initially",
);
});
cx.dispatch_action(ToggleSoftWrap);
cx.run_until_parked();
editor.update_in(cx, |editor, _, cx| {
assert!(
matches!(editor.soft_wrap_mode(cx), SoftWrap::EditorWidth),
"toggling soft wrap while the search bar is focused should affect the searched editor",
);
});
search_bar.update_in(cx, |search_bar, window, cx| {
assert!(
search_bar.query_editor.focus_handle(cx).is_focused(window),
"focus should stay in the search bar",
);
});
}
#[cfg(target_os = "macos")]
#[gpui::test]
async fn test_cmd_e_then_cmd_g_uses_selection_for_find(cx: &mut TestAppContext) {