Skip to content

editor: keep wrapped search matches visible with context - #3013

Merged
huacnlee merged 6 commits into
longbridge:mainfrom
hlcfan:surrounding-lines
Sep 10, 2026
Merged

huacnlee merged 6 commits into
longbridge:mainfrom
hlcfan:surrounding-lines

Conversation

@hlcfan

@hlcfan hlcfan commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Description

Keep search matches visible with surrounding context when long lines wrap. Resolve matches to their display rows, including offscreen matches, and calculate padding from viewport capacity instead of visible logical-line count.

Screenshot

Before After
Screenshot 2026-09-08 at 7 33 40 PM Screenshot 2026-09-08 at 7 34 56 PM

How to Test

  • cargo test -p gpui-base --lib input:: --offline — 220 tests passed, including the wrapped-match regression test.
  • cargo run -p example-editor --offline — built and launched on macOS.
  • With soft wrapping enabled, search for a word near the end of a long offscreen line. Navigate to the match and verify it stays visible with surrounding rows. E.g. "path": "relative_path:asdfasfafadsf[?(@['username'] == 237842985798579834534536363365")]"

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

@hlcfan hlcfan changed the title fix(editor): keep wrapped search matches visible with context editor: keep wrapped search matches visible with context Sep 8, 2026
@huacnlee

huacnlee commented Sep 8, 2026

Copy link
Copy Markdown
Member

Code review

No blocking issues found. Checked for bugs, CLAUDE.md compliance, git history context, and comment consistency.

The root cause fix looks right: visible_range counts logical lines (element.rs documents it as "based on unwrapped lines"), so one long wrapped line collapsed it to 1-2 and starved the cursor_surrounding_lines margin. Deriving capacity from bounds.size.height / line_height fixes the unit mismatch, and switching to buffer_pos_to_display_pos resolves the wrapped sub-row through the DisplayMap wrap cache rather than last_layout, so offscreen matches work. Dropping row_offset_y += pos.y is correct — the new call already includes the sub-row offset. This generalizes #2955, whose regression test used non-wrapping lines where logical and display rows coincide.

Two optional, non-blocking notes:

  1. (bounds.size.height / line_height) as usize is now duplicated verbatim at both call sites. The coding guides ask to centralize shared geometry, and the adjacent comment already says the two paths must agree — a small shared helper would enforce that.

let top_bottom_margin = cursor_surrounding_padding(
state.mode.is_auto_grow(),
state.cursor_surrounding_lines,
(bounds.size.height / line_height) as usize,
line_height,
);

super::element::cursor_surrounding_padding(
self.mode.is_auto_grow(),
self.cursor_surrounding_lines,
(bounds.size.height / line_height) as usize,
line_height,
)

  1. Pre-existing, but two lines above a modified line: the comment references TextElement::layout_cursor, renamed to layout_cursors in f6e2ed6. Cheap to fix while here.

// Scroll the row into view. Use the same edge clearance helper as
// `TextElement::layout_cursor` so both scroll-into-view paths agree
// (a mismatch flickered on `Down` at end-of-buffer with a small
// `cursor_surrounding_lines` override).

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@huacnlee
huacnlee enabled auto-merge (squash) September 10, 2026 07:23
@huacnlee

Copy link
Copy Markdown
Member

Thanks

@huacnlee
huacnlee merged commit b207a6d into longbridge:main Sep 10, 2026
9 checks passed
@hlcfan
hlcfan deleted the surrounding-lines branch September 10, 2026 09:52
linruohan pushed a commit to linruohan/gpui-component that referenced this pull request Sep 11, 2026
…3013)

## Description

Keep search matches visible with surrounding context when long lines
wrap. Resolve matches to their display rows, including offscreen
matches, and calculate padding from viewport capacity instead of visible
logical-line count.

## Screenshot

| Before | After |
| ------ | ----- |
| <img width="592" height="863" alt="Screenshot 2026-09-08 at 7 33
40 PM"
src="https://github.com/user-attachments/assets/cafa15d3-d78f-460f-a200-e5f0f2fd913b"
/> | <img width="592" height="863" alt="Screenshot 2026-09-08 at 7 34
56 PM"
src="https://github.com/user-attachments/assets/d0ee0ddf-3d00-41df-834f-5f11a10502f6"
/> |

## How to Test

- `cargo test -p gpui-base --lib input:: --offline` — 220 tests passed,
including the wrapped-match regression test.
- `cargo run -p example-editor --offline` — built and launched on macOS.
- With soft wrapping enabled, search for a word near the end of a long
offscreen line. Navigate to the match and verify it stays visible with
surrounding rows. E.g. `"path":
"relative_path:asdfasfafadsf[?(@['username'] ==
237842985798579834534536363365")]"`

## Checklist

- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [ ] Reviewed the changes in this PR and confirmed AI generated code
(If any) is accurate.
- [x] Passed `cargo run` for story tests related to the changes.
- [ ] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
huacnlee added a commit to calmondev/gpui-component that referenced this pull request Sep 17, 2026
The search state and the public search API this branch introduced were
implemented on main in the meantime (longbridge#2691, longbridge#2716, longbridge#2932, longbridge#2953, longbridge#2955,
longbridge#3013), so the conflicting files take main's version. The example and
the documentation are ported to the new API in follow-up commits.
huacnlee added a commit that referenced this pull request Sep 17, 2026
## Description

An application that wants its own search UI on the editor — keeping the
editor's matching, highlighting, scrolling and replacing, but drawing
the search bar itself — had no supported way to do it: search could only
be switched on or off with `.searchable(true)`, and everything else
lived inside the built-in `SearchPanel`.

Since this PR was opened, the engine moved into `gpui-base` and its API
became public on the editor state (#2691, #2716, #2932, #2953, #2955,
#3013): `set_search_query`, `close_search`, `next_search_match`,
`previous_search_match`, the replace methods and `search_session()`.
This PR, rebased onto that, closes the gaps that still tied a search to
the built-in panel and documents the custom-UI path.

### Highlights without the panel

Match highlights were painted only while `search_session.open`, which
also shows the panel, so a query set through `set_search_query` counted
matches the editor never showed. `SearchSession` now tracks `active`
separately from `open`: opening the panel or setting a query activates
the session, `close_search` ends it, and the element highlights while it
is active. The panel keeps keying its own visibility on `open`.
`SearchSession` is `#[non_exhaustive]`, since it gains fields; build it
with `Default`.

### The shortcut reaches the application

An editor that is not `searchable` swallowed `Search` and `Replace`
instead of propagating them, so a parent view could not bind `Ctrl-F` /
`Cmd-F` to its own field. Both handlers now `cx.propagate()` when the
editor is not searchable. `searchable` is no longer `#[doc(hidden)]` and
is documented for what it gates: the built-in panel and its shortcut,
not the search API.

### `current()`

`SearchMatcher::current()` reports the current match as `Option<usize>`,
so "no match" is not mistaken for the first one; `current_match_index()`
stays.

### Docs

The Editor guide gains a "Custom search UI" section, in English and
Chinese: `set_search_query` from the application's own field, navigating
and describing the matches, replacing, ending with `close_search`, and
taking `Ctrl-F` / `Cmd-F` on the view that owns the field.

## Breaking Changes

None. `SearchSession` is now `#[non_exhaustive]`, so it must be built
with `Default` rather than a struct literal; no code in this repository
or its examples did otherwise.

## How to Test

- `cargo test -p gpui-base -p gpui-component --lib -- search`: covers
the session staying active after `set_search_query` without the panel,
the shortcut reaching a host view when the editor is not searchable and
opening the panel when it is, and the match label/`current()` semantics.
- `cargo run -p example-editor`: the built-in panel behaves as before,
including resuming its last query on reopen.

The rebase, the Base changes and the documentation were written with
Claude Code and reviewed by hand; the original API proposal is by
@calmondev.

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants