Closes#2544
## Description
PR to solve selection update on editor component.
## Screenshot
## Break Changes
No breaking changes
## How to Test
Execute test crates/ui/src/input/state.rs - test_set_selection
## Checklist
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] 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.
- [x] _Tested macOS_
- [ ] , Windows and Linux platforms performance (if the change is
platform-specific)
---------
Co-authored-by: Jason Lee <huacnlee@gmail.com>
## Description
These two changes are related, so I kept them in the same PR but
different commits, so it can be reviewed as two.
This is done to improve editing in larger files, but improving this also
improves overall editing.
After this change, editing the example json file is faster than Zed
(ironically since this uses their sum_tree crate π). Feels on par
with Vscode when forcefully enabled highlighting but uses less memory.
### Wrapping refactored to a SumTree
Replaces the `Vec<LineItem>` with a `SumTree<LineItem>`. The vec
required O(n) scans for things like row offset, cursor placement or
finding the longest line. With SumTree it becomes O(log n) or even O(1)
depending on if it's cached. Similar to diagnostics that is already
backed by a SumTree. One of the biggest wins is editing though since we
no longer need to splice the vec in text_wrapper
(`self.lines.splice(rows_range, new_lines);`)
Call sites that previously did `display_map.lines().get(row)` or
`.iter().enumerate()` now go through `display_map.line(row)` and
`buffer_line_to_display_row(row)`, which resolve via the tree.
### Skip foreground parsing on large files
For large files I've added a threshold for foreground parsing, since we
don't even need to attempt it if the file is big enough. It will always
time out anyway.. Also attempts to cancel any previous parses, this
could be further improved with a parse lock to absolutely prevent
parallel parsing.
## Screenshot
### Before
https://github.com/user-attachments/assets/58f94c40-a61c-4c8b-8562-fea428e6f17d
### After
https://github.com/user-attachments/assets/dc3f5ce7-b790-4835-8c49-0b514797b185
### Zed comparison
https://github.com/user-attachments/assets/5b7351fd-9f66-497f-a0d7-93eab827842f
This is with the document color provider and linting disabled in the
editor. Also folding is disabled because it still is a bit slow for
these huge files.
## How to Test
- Generate test file
```sh
$ (echo '['; for i in $(seq 1 999999); do echo "{\"id\": $i, \"name\": \"user_$i\"},"; done; echo '{"id": 1000000, "name": "user_1000000"}'; echo ']') > big_test.json
```
- Disable/comment lint_document and document_color_provider in editor.rs
```diff
--- a/crates/story/examples/editor.rs
+++ b/crates/story/examples/editor.rs
@@ -713,7 +713,7 @@ impl Example {
editor.lsp.code_action_providers = vec![lsp_store.clone(), Rc::new(TextConvertor)];
editor.lsp.hover_provider = Some(lsp_store.clone());
editor.lsp.definition_provider = Some(lsp_store.clone());
- editor.lsp.document_color_provider = Some(lsp_store.clone());
+ // editor.lsp.document_color_provider = Some(lsp_store.clone());
editor
});
@@ -731,7 +731,7 @@ impl Example {
Self::load_files(tree_state.clone(), PathBuf::from("./"), cx);
let _subscriptions = vec![cx.subscribe(&editor, |this, _editor, _: &InputEvent, cx| {
- this.lint_document(cx);
+ // this.lint_document(cx);
})];
Self {
```
- Run editor cargo run --release --example editor
- Disable folding (still a bit slow on these large files)
## Checklist
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] 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>
Closes#2508
## Description
Fix `.overflow_y_scrollbar()` / `.overflow_scrollbar()` layout behavior
when used with layout styles such as `.gap()`.
Previously, `Scrollable` rendered an additional internal scroll-area
`div` and inserted the caller's element as a child of that wrapper. This
changed the layout structure and could cause styles applied to the
original element, such as flex gap, to no longer behave as expected.
This PR changes `Scrollable` so the original element itself becomes the
scroll-tracked container. The outer wrapper is only used to preserve
sizing and overlay the scrollbar. This keeps the caller's layout styles
intact while still rendering the scrollbar.
Main changes:
* Preserve the source element as the actual scroll area.
* Move scroll tracking and overflow behavior directly onto the source
element.
* Keep the outer wrapper responsible for sizing, positioning, and
scrollbar overlay.
* Use generated element IDs instead of fixed string IDs for scrollable
parts.
* Generalize the `InteractiveElement` implementation for
`Scrollable<E>`.
## Screenshot
| Before | After |
| ----------------------------- | ------------------------------- |
| See issue #2508 actual result | See issue #2508 expected result |
## How to Test
1. Run a story or example that uses `.overflow_y_scrollbar()` with a
flex layout and `.gap()`, for example:
```rust
v_flex()
.flex_1()
.gap(px(30.))
.overflow_y_scrollbar()
.px(px(12.))
.pb(px(16.))
.children(cards)
```
2. Confirm that the gap between children is preserved.
3. Confirm that the vertical scrollbar still appears and scrolls
correctly.
4. Confirm that existing `.overflow_scrollbar()`,
`.overflow_x_scrollbar()`, and `.overflow_y_scrollbar()` usages still
work.
## Checklist
* [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
* [x] 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)
## Description
Adds a `reverse()` builder method to `Slider`. By default the track is
filled from the min end to the thumb; with `reverse`, the fill goes from
the thumb to the max end instead β useful when the slider represents a
remaining amount (e.g. time left in a media player).
```rust
Slider::new(&state).horizontal().reverse()
```
This only changes the visual fill; values, events and interactions are
unaffected. It applies to single-value sliders and is ignored for range
sliders (a range slider's fill is already bounded by both thumbs).
Also adds a "Reverse Slider" section to the slider story to showcase the
new option.
No breaking changes.
<img width="1642" height="1106" alt="image"
src="https://github.com/user-attachments/assets/c107d7e3-9856-4d27-a3c2-360d2d7ac0df"
/>
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
Closes#2534.
Users read the `anchor` option as "which side of the trigger the popover
shows on" and reported it as inverted. It actually uses GPUI's
corner-anchoring model, and the docs described it in a way that
reinforced the wrong mental model.
This PR only clarifies documentation, no behavior change. Instead of
explaining the mechanics, it uses a concrete image: imagine the popover
has a pointer tip (like a speech bubble's tail) β the anchor is where
that tip sits relative to the trigger (`Anchor::TopLeft` β the trigger's
top-left corner, `Anchor::BottomRight` β the bottom-right, and so on),
and the popover hangs off that point.
- Rewrite the `anchor()` doc comments on `Popover` and `HoverCard`.
- Update the Popover and HoverCard docs (en + zh-CN) with the same
explanation.
- Drop stale wording that implied two separate `Anchor` types.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#2476
## Description
The `InputState.lsp` provider fields are public, but assigning them at
runtime does not refresh the editor until the text next changes.
This adds `InputState::refresh(cx)`, which flags a pending update so the
next render re-runs syntax highlighting and the LSP providers (not just
a redraw).
```rust
input.update(cx, |state, cx| {
state.lsp.hover_provider = Some(provider);
state.refresh(cx);
});
```
## How to Test
Create an InputState, assign an `lsp` provider at runtime, then call
`refresh` and watch the input update without editing text.
- Attach an LSP provider (hover / document color / semantic tokens) at
runtime, call `refresh`, confirm it takes effect without editing text
- Swap providers at runtime and `refresh`; confirm old results clear and
the new provider runs
## Checklist
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] 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>
Co-authored-by: ScottCUSA <ScottCUSA@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
### Problem
Markdown inline highlighting could break across list items. Code span
highlighting could stop after the first list item, then apply to later
plain text until another code span appeared.
### Solution
Preserve the separator text between combined markdown inline ranges
before parsing them. This keeps each list item separated during
highlighting. Added a regression test for code spans in markdown list
items.
### Testing
- All highlighter tests (including a new one)
- Manual testing via `cargo run -p gpui-component-story --example
markdown`
---
Implemented-by: Codex 5.5
Reviewed-by: Claude Opus 4.8
---------
Co-authored-by: Jason Lee <huacnlee@gmail.com>
## Description
Make tree-sitter optional
## How to Test
```sh
cargo check -p gpui-component --no-default-features
cargo check -p gpui-component --features tree-sitter-languages
cargo check -p gpui-component --target wasm32-unknown-unknown
cargo check -p gpui-component --target wasm32-unknown-unknown --features tree-sitter-languages
cargo check --workspace
cargo test -p gpui-component --no-default-features
cargo test -p gpui-component --features tree-sitter-languages
```
## Checklist
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] 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>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Description
Add new function `replace_all` to replace input value while preserving
undo history.
## Break Changes
Add new API `replace_all` to input state.
## How to Test
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce.
```
test_replace_all_single_line
test_replace_all_multi_line
test_replace_all_preserves_undo_history
```
## Checklist
- [ ] 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.
- [ ] Passed `cargo run` for story tests related to the changes.
- [ ] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)
## Description
Fixes a potential `NaN` propagation in
`ResizableState::adjust_to_container_size`.
When all panel sizes sum to `0` (e.g. before the first layout pass, or
when panels were collapsed to zero), the `size / total_size` ratio
becomes `NaN` (`0 / 0`), and every panel size gets poisoned with `NaN`
values that then persist in the state and break subsequent resizing.
Now the adjustment is skipped when the total size is not finite or not
positive, keeping the previous sizes until valid measurements are
available.
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Reorders some of the layout code in `TextElement::prepaint` to compute
the soft wrap width and update the display map so the rest of the layout
and painting have the correct soft wrapping in the current frame.
Closes#2520
## Screenshot
| Before | After |
| ---------------------------- | --------------------------- |
| <video
src="https://github.com/user-attachments/assets/474f6dd1-8f82-4ad0-a1a1-764b0e4498b4"></video>
| <video
src="https://github.com/user-attachments/assets/62c6a66e-91d1-4a6a-83ce-271dd7a71809"></video>
|
*Notice in the top textbox the text no longer jumps into place after the
mouse is moved.*
## Checklist
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] 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)
## Description
Problem: gpui-component's InputState::handle_mouse_move calls
cx.notify() unconditionally, and GPUI's mark_view_dirty() propagates
this up the dispatch tree to MainView.
Cause: InputState::handle_mouse_move in gpui-component calls cx.notify()
on every mouse movement pixel, even when no LSP hover state changes.
GPUI's mark_view_dirty() walks the dispatch tree upward, marking all
ancestors dirty - causing MainView to re-render the entire tree on each
mouse move.
Fix: Only cx.notify() when hover_definition or hover_popover actually
changed.
## Checklist
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] 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.
- [x] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)
---------
Co-authored-by: Floyd Wang <gassnake999@gmail.com>
## Description
Adds support for the HTML `<mark>` tag in the text/HTML renderer,
rendering marked text with a background highlight.
The highlight color is customizable via the `color` attribute or the
`background-color` style declaration, parsed by the existing
`try_parse_color` helper:
```html
<mark>highlighted</mark> <!-- default light yellow -->
<mark color="blue-200">blue tint</mark> <!-- Tailwind scale -->
<mark color="amber/30">amber, 30% opacity</mark> <!-- opacity modifier -->
<mark style="background-color: #fca5a5">custom hex</mark> <!-- hex -->
```
- Accepts hex (`#3366ff`), named colors (`blue`), Tailwind scales
(`blue-200`), and opacity (`amber/30`).
- Defaults to a light yellow highlight when no color is given.
`TextMark` gains an additive `highlight: Option<Hsla>` field (private
module, not part of the public API). The `<mark>` demos were added to
the HTML example fixture, with a unit test covering the tag.
> Note: AI-generated code, refactored to match project style.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Darken the dashed crosshair by blending `border` toward `foreground`,
so it reads a touch deeper than the (border-colored) grid lines in both
light and dark themes.
- Fix the horizontal bar hover band overflowing the value axis: it
called `.span()` (which sets the vertical span) after `.horizontal()`,
so the horizontal line fell back to `w_full()`. Use `.h_span()` to
confine the band to the bar region.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Description
Fixes the Table selection background becoming invisible after #2484.
`Background::opacity` *multiplies* the existing alpha, but the token
clamping in `ThemeColor` passed the absolute target alpha as the
multiplier factor. The default `table.active.background` (`#bfdbfe33`)
already carries `0.2` alpha, so it was attenuated twice (`0.2 Γ 0.2 =
0.04`) and the active row highlight became nearly invisible.
The fix passes a factor (`target / base`) instead, so the final alpha
lands on the clamped target regardless of the base alpha, and gradient
stops keep their relative opacity. The same latent bug applied to
`list_active` (only invisible because components read the `Hsla`, not
the token background) and `selection` (correct only by luck, since its
default base alpha is `1.0`), so all three now go through one shared
`clamp_alpha` helper.
| Before | After |
| - | - |
| <img width="2802" height="1764" alt="image"
src="https://github.com/user-attachments/assets/3afe3087-229c-45b6-aa6c-4ea51edcb613"
/> | <img width="2802" height="1764" alt="image"
src="https://github.com/user-attachments/assets/d85e10b2-c29d-4d8e-9135-9974279c32a5"
/> |
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Floyd Wang <gassnake999@gmail.com>
## Description
Ensure the cursor is at the start when setting text. This fixes the
issue when an input value is long, it shows the latter part of the value
after setting value.
## How to Test
`test_set_value_move_cursor_to_start`
## Checklist
- [ ] 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.
- [ ] 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: Floyd Wang <gassnake999@gmail.com>
Description
Follow-up fixes and polish for the interactive chart hover tooltips
added in #2500.
- **Tooltip vertical stretch** β a stray `top_0()` on the tooltip `base`
stretched the box vertically; removed it.
- **Tooltip clipped under later siblings** β the overlay was painted
inline during the plot's `paint`, so siblings drawn afterwards (e.g. a
chart card footer) covered the part of the box overflowing the plot
bounds. It's now wrapped in `gpui::deferred(...)` so it paints on top;
`prepaint_as_root` keeps the plot-origin offset, so positioning is
unchanged.
- **Downstream clippy lint** β the `IntoPlot` derive emitted
`let...else`, which tripped `clippy::question_mark` in crates that
derive it. It now emits `?` so generated code is lint-clean.
- **`CrossLine::both()` crosshair** β both axes shared a single span, so
confining the crosshair with `height()`/`span()` clipped the horizontal
line to the vertical extent. Each axis now confines independently.
## Breaking Changes
- `CrossLine::height()` / `span()` now confine only the **vertical**
line. Use the new `width()` / `h_span()` to confine the **horizontal**
line.
```diff
- CrossLine::new(point).horizontal().height(120.)
+ CrossLine::new(point).horizontal().width(120.)
```
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
# Fix Async Patterns in GPUI Documentation
## Description
This PR fixes the async spawn examples in
`skills/gpui/references/async.md` that contained incorrect patterns
leading to runtime panics. The examples were using tokio's sleep
functions (`tokio::time::sleep`) within GPUI's async context, which is
incorrect. The correct pattern is to use GPUI's built-in timer
functionality via `cx.background_executor().timer()`.
**Problem Solved:**
- Removed dependency on tokio for sleep functionality in GPUI async
examples
**Benefits:**
- Example will run without panics
- Demonstrates the correct GPUI async pattern
## Screenshot
| Before | After |
| ---------------------------- | --------------------------- |
| [Runtime panic on async spawn] | [Successfully runs and updates UI] |
## Break Changes
None. This is purely a documentation fix that corrects the example code.
---
**Correct Async Pattern Example:**
```diff
- // β Incorrect: Uses tokio sleep and wrong spawn signature
- cx.spawn(...{
- tokio::time::sleep(std::time::Duration::from_secs(2)).await;
- cx.update(|cx| {
...
- }).ok();
- });
+ // β Correct: Uses GPUI's timer and proper spawn signature
+ cx.spawn(async move |this, cx| {
+ cx.background_executor()
+ .timer(std::time::Duration::from_secs(2))
+ .await;
+
+ this.update(cx, |this, cx| {
...
+ }).ok();
+ });
```