## Summary
- Document pricing and Zed-hosted context limits for Claude Fable 5,
Claude Sonnet 5, and GPT-5.6 Sol, Terra, and Luna.
- Record recent Gemini and xAI model retirements and replacement
guidance.
- Put the Fable safety-retention warning on the hosted-model reference
and align the privacy docs with the current Anthropic Covered Models
terminology and retention period.
## Sources
- zed-industries/cloud origin/main at
f5d109a868a303241e89ea30d6da8da19699ead4
- OpenAI GPT-5.6 model documentation
- Anthropic Covered Models retention policy
- xAI May 15, 2026 model retirement guide
## Testing
- pnpm dlx prettier@3.5.0 . --check
- mdbook build docs
Release Notes:
- N/A
Closes security loopholes and updates docs:
- installs seccomp filter for blocking naughty syscalls
- tightens macos seatbelt profile
- fetch tool responses that redirect are now constrained by allowed
domains list
Also adds a few "Learn More" buttons that link to the new docs.
Also fixes a bug where the agent would try to create a
`~/.config/zed/AGENTS.md` directory
Also adds unicode confusable detection to URL/path privilege escalation
prompts.
---
Release Notes:
- N/A or Added/Fixed/Improved ...
---------
Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
The `edit_file` tool re-indents `new_text` by computing a single indent
delta from the first line of `old_text` versus the matched buffer line,
then applying that delta to every line of the replacement. When a model
omits the leading indentation on only the first line of
`old_text`/`new_text` (a common pattern when copying from mid-line
context), the delta computed from the first line was wrongly applied to
the remaining, already-correctly-indented lines, doubling their
indentation.
This PR:
- Tracks the `(query_row, buffer_row)` line pairs aligned by the
streaming fuzzy matcher so the indent of each `old_text` line can be
compared against the buffer line it actually matched.
- Computes a separate indent delta for the lines after the first: when
those lines agree on a consistent delta, it's used for the rest of the
replacement; otherwise the previous uniform behavior is preserved.
- Keeps `query_lines`/line pairs in sync when `finish()` extends a match
with a trailing incomplete line.
- Adds an end-to-end regression test reproducing the issue, plus unit
tests for the new re-indentation logic.
Closes https://github.com/zed-industries/zed/issues/60302
Release Notes:
- Fixed the agent's `edit_file` tool corrupting indentation when a
replacement omitted leading whitespace on only its first line.
On Linux and FreeBSD the native file watcher is non-recursive, so a
watch on the .git directory itself does not report changes to files
nested below it. Loose refs live in nested directories under refs, so
external git commit, fetch, branch, and update-ref operations that don't
also touch a direct child of .git (like the index) went entirely
unnoticed.
Watch every directory in the refs tree when a repository is inserted,
and watch directories subsequently created under refs (new remotes,
slash-named branches) as their creation events arrive. On platforms with
recursive watchers these registrations dedupe against the existing
recursive watch, making them free.
---
Release Notes:
- N/A or Added/Fixed/Improved ...
# Objective
Fixes#59820.
## Solution
At a high level, I approached this by conditionally rendering a `Below`
preview layout only when the preview was at or below the min width of
the `Right` when the layout is `Right`. Since there's one breakpoint at
`SizeBounds::min_width()` and hitting that breakpoint only changes
rendering (and doesn't change the state), there's no need to persist to
storage or memory that the view is forced to be Below.
I toggled between the two rendering modes by defining
`Picker::preview_layout_rendered()` that would return `Below` if the
window was equal to or smaller than its min width. Then I replaced all
calls of `preview_layout()` that used the response purely for rendering
purposes with `preview_layout_rendered()`. I avoided replacing
`preview_layout()` calls that were responsible for persisting the
current layout and window dimensions to disk to ensure that changes to
the window dimensions were properly synced to the correct layout. This
way changes to the window while in the `BelowAuto` rendering mode edit
the `Right` window dimensions on disk.
## Testing
I tested these changes manually with the following actions on my local
machine (an Apple Silicon Macbook Pro):
- resizing the popover
- resizing the window
- adjusting the split
- making sure the popover size and split percent remains the same after:
- closing and re-opening the popover
- closing and re-opening the application and making sure the window size
and split percent remains the same
Reviewers can test my changes by performing the same manual actions.
## Self-Review Checklist:
- [X] I've reviewed my own diff for quality, security, and reliability
- [X] Unsafe blocks (if any) have justifying comments
- [X] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [X] Tests cover the new/changed behavior
- [X] Performance impact has been considered and is acceptable
## Showcase
### Before
https://github.com/user-attachments/assets/a4524065-d1a3-4595-bc9e-50c9715b4e66
### After
https://github.com/user-attachments/assets/fede7eb0-5d73-423c-8df0-8192caee8d26
---
Release Notes:
- fix(picker): Automatically switch from Right split to Down split when
picker gets too narrow
LM Studio doesn't show the context token wheel (#53790) because token
usage is
never reported in streaming responses.
Causes:
1. `stream_options` was missing from the request. Without
`stream_options: { include_usage: true }`, the LM Studio API omits
`usage`
from every streaming chunk entirely.
2. The event mapper discarded usage data in the final chunk.
OpenAI-compatible
servers send the usage summary in a trailing chunk that has an empty
`choices`
array. The old guard treated that as an error, so even when usage was
present
it was thrown away before emitting a `UsageUpdate` event.
Fix:
- Add `StreamOptions { include_usage: bool }` and `stream_options` to
`ChatCompletionRequest`, and always set it to `true` for streaming
requests.
- Move usage handling in `LmStudioEventMapper::map_event` to run
*before* the
empty-choices guard, mirroring the OpenAI provider's approach.
- Add four unit tests for `map_event` covering the fixed behavior.
Release Notes:
- Fixed LM Studio not showing the context window usage wheel.
<img width="1184" height="1080" alt="Screenshot_20260527_130449"
src="https://github.com/user-attachments/assets/97eb8500-39dd-4824-aaf8-f0422b62119d"
/>
---------
Co-authored-by: Gabriele Ancillai <gabriele.ancillai@sofka.com.co>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Adds support for enabling Anthropic fast mode on configured models.
Previously, Anthropic models configured through
`language_models.anthropic.available_models` were always marked as not
supporting fast mode, so `speed: "fast"` would be stripped before
sending requests even when the configured model supported Anthropic fast
mode.
This adds an optional `supports_fast_mode` field to configured Anthropic
models. When enabled, the model is marked as supporting fast mode and
the required Anthropic beta header is added automatically. Built-in
fast-mode model detection remains the fallback when the setting is
omitted.
Testing:
- `cargo fmt --package anthropic --package language_models --package
settings_content`
- `cargo test -p language_models available_model --lib`
- `cargo test -p anthropic from_listed_enables_fast_mode --lib`
- `cargo check -p language_models`
- Manual: verified in a local build that a configured Anthropic model
can send fast mode requests correctly.
Release Notes:
- agent: Allow specifying if fast mode is supported for custom anthropic
models
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This passes the `include_remote_name` flag through the remote
`GetDefaultBranch` RPC instead of dropping it at the client/host
boundary. That lets remote repositories resolve default branches as
`origin/main` when needed, so worktree creation uses a valid remote
branch name instead of falling back to `main`.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes#59121
Release Notes:
- Fixed remote worktree creation from the default branch when the
default branch requires its remote name.
---------
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Closes#21822
The issue is closed already, but it was requested to provide the
`hard_tabs` boolean param.
Follow-up to #52175
Reviewed by @maxdeviant (it already bumped the settings path to v0.8.0,
so this `hard_tabs` change only needs 2 files, 2-3 lines of code
additions).
## Context
Exposes `hard_tabs` from `AllLanguageSettings` to the Extension
API's `LanguageSettings` struct. Currently, only `tab_size` and
`preferred_line_length` are available to extensions, which prevents
language extensions (e.g., Go, C++) from reading the user's hard tabs
preference and forwarding it to their language server or formatter
(e.g., as rustfmt.hard_tabs or clang-format.UseTab).
Related:
https://github.com/zed-industries/zed/issues/21822#issuecomment-4139809956
## How to Review
Small change — follow how `tab_size` is plumbed through:
1. WIT definition (`language-settings` record)
2. `extension_api` Rust struct
3. Host-side bridge conversion
The new field follows the exact same pattern.
## Self-Review Checklist
<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- Compile-time verified via WIT bindings; no runtime behavior change
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
Extends the existing ECMAScript comment-label injection system to
support GLSL and WGSL shader languages inside JS/TS strings and template
literals.
The pattern `/* language */` before a string is already used for `html`,
`sql`, `graphql`, and `css`. This adds the same support for two shader
languages commonly embedded as strings in WebGL/WebGPU code:
before:
<img width="460" height="300" alt="Screenshot 2026-04-30 at 23 07 05"
src="https://github.com/user-attachments/assets/8952eddc-2f53-42ea-b086-7a0b63dceb10"
/>
after:
<img width="460" height="300" alt="Screenshot 2026-04-30 at 23 07 39"
src="https://github.com/user-attachments/assets/93bf536a-9df4-438b-af7b-382efda196ca"
/>
Syntax highlighting activates when the corresponding extension is
installed:
- GLSL: the built-in `glsl` extension
- WGSL: the community extension
[`wgsl-wesl-zed`](https://github.com/lucascompython/wgsl-wesl-zed)
(language name `WGSL/WESL`)
Both `/*glsl*/` and `/* glsl */` forms are accepted (same behavior as
the existing patterns).
Release Notes:
- Added `/*glsl*/` and `/*wgsl*/` comment-label syntax injection for
JavaScript and TypeScript template literals
Turns out I only tested it with all tabs closed, the keybinding would
only work when all tabs were closed and you focused the agent panel. If
a file was opened in the center pane, cmd-f would open a search in that
file instead of searching in the agent panel, even if the thread view as
focused.
Closes#60686
Release Notes:
- agent: Fixed an issue where cmd-f would not work if file is open in
center pane
## Context
On Linux, `ctrl-q` is globally bound to `zed::Quit`. When a
`TerminalView` is focused, pressing `ctrl-q` quit the application
instead of forwarding the keycode to the shell, breaking programs like
`ftp`, `tig`, and any app that uses XON/XOFF flow control.
Windows already had the fix: its `Terminal` keymap context overrides
`ctrl-q` with `["terminal::SendKeystroke", "ctrl-q"]`. The Linux keymap
was simply missing that override.
Closes#58809
Manual test after fix below :
[Screencast from 2026-06-09
00-46-37.webm](https://github.com/user-attachments/assets/3d103b2a-bff1-4559-af1d-2a52d57a6b18)
## How to Review
- **`assets/keymaps/default-linux.json`** : One-line addition in the
`Terminal` context under the "Overrides for conflicting keybindings"
comment, mirroring the existing Windows entry.
- **`crates/terminal_view/src/terminal_view.rs`** : Regression test
`ctrl_q_is_forwarded_to_terminal_not_quit` (Linux-only, `#[cfg(target_os
= "linux")]`): loads the default keymap, focuses a display-only
terminal, simulates `ctrl-q`, and asserts the PTY receives byte `0x11`
instead of the quit action firing.
## Self-Review Checklist
- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- Fixed `ctrl-q` quitting Zed instead of being forwarded to the shell
when a terminal is focused on Linux
Removes the feature flag. The RFD is in Preview and I am confident it
will be stable without major changes by the time this hits Zed Stable.
Release Notes:
- acp: Allow ACP agents to use Elicitation capturing structure user
input.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes#57174
Release Notes:
- Open non-writeable files in Capability::Read mode
Co-authored-by: Lukas Wirth <lukas@zed.dev>
# Objective
- Fixes#58097.
- Opening a new project and clicking `+` in the agent panel to start a
terminal thread creates two terminals instead of one. I am able to
replicate the issue on version 1.8.0 on macOS 27
## Solution
The new-thread action creates the terminal and then focuses the agent
panel. Focusing re-activates the panel (`Panel::set_active` then
`ensure_thread_initialized`) before the terminal, which is spawned
asynchronously, has registered. The panel still looks uninitialized at
that moment, so it spawns its own "initial" terminal too, and that is
the duplicate.
`spawn_terminal` now marks the spawn as in-flight
(`pending_terminal_spawn`) the moment it starts, the same way the
restore and initial-terminal paths already do, so the existing guard in
`ensure_thread_initialized` skips the redundant terminal.
This only affected new (unrestored) projects, since existing ones
restore their previous entry instead of auto-creating one. The auto-init
behavior was introduced in #57150.
## Testing
- Verified in a local dev build on macOS: opening a fresh project and
clicking `+` now creates one terminal, and clicking `+` again creates a
second, as expected. Reopening an existing project still restores a
single terminal.
- Added `test_explicit_terminal_blocks_redundant_auto_init`, which fails
without the fix.
- The change is platform-agnostic (no platform-specific code); I wasn't
able to test on Linux/Windows.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- Fixed a duplicate terminal being created when starting an agent
terminal thread in a new project
### Closes#51951
## Self-Review Checklist
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
#### Note : Reopens previous work from closed PR
https://github.com/zed-industries/zed/pull/52161 (fork was deleted)
## Video
[Screencast from 2026-03-22
23-26-06.webm](https://github.com/user-attachments/assets/ab68e47a-7e74-4f1e-991d-8ca4fed7952c)
## Release Notes:
- Fixed MCP servers from `.zed/settings.json` not being discovered when
multiple project folders are open in a workspace.
---------
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Release Notes:
- agent: Added GPT 5.6 Sol & Terra for ChatGPT subscription. Note: GPT
5.6 Luna is not available yet, since OpenAI has not unlocked access for
third-party clients
Dropping a scheduled runnable cancels its task and makes the next poll
of any awaiter panic with "Task polled after completion." The only paths
where we drop these runnables seem to be during shutdown or extreme
resource exhaustion, so, let's leak the runnables instead of crashing.
On Windows, we also moved to calling the Win32 thread pool API directly,
because 1) WinRT thread pool API is just a wrapper that adds overhead we
don't need, and 2) the closure we pass to the `WorkItemHandler` object
takes ownership of the runnable object, so if the WinRT thread pool
releases the delegate, it can free the runnable without our control.
Release Notes:
- N/A
This PR fixes a few History tab edge cases in the Git Panel.
For a fresh repo with no commits, the History tab now finishes loading
and shows No commits yet instead
of sitting on Loading… indefinitely or falling into a misleading
empty/error state.
It also fixes detached HEAD history loading. In that case, the Git Panel
asks the backend to load history
from the current commit SHA. The local git backend was accidentally
treating the raw object ID bytes as a
string instead of formatting them as a normal hex SHA, so git log could
fail before returning any
commits. The backend now passes the SHA in the format git expects.
**Repro for empty repo:**
mkdir /tmp/zed-empty-history
cd /tmp/zed-empty-history
git init
zed .
Open Git Panel → History.
Before: History could stay stuck on Loading….
After: History shows No commits yet.
**Repro for detached HEAD:**
mkdir /tmp/zed-detached-history
cd /tmp/zed-detached-history
git init
echo hi > file
git add file
git commit -m initial
git checkout --detach HEAD
zed .
Open Git Panel → History.
Before: History could fail to load commits.
After: History shows the commit history normally.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- Fixed Git history tab states for empty repositories and detached HEAD
history.
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
Add new GPT 5.6 models
Release Notes:
- open_ai: Added support for GPT 5.6 Sol/Terra/Luna
---------
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This makes them more consistent with regards to limiting the label size.
These two tabs are similar because they frequently house a label that's
pretty big, and the agent diff tab didn't truncate it in any way.
Release Notes:
- N/A
What looks like a single event of applying a few labels is actually
multiple events, and the raciness is racy as we've recently experienced.
One solution would've been adding a “first responders notified” label
for enforcing consistency but issues already have enough labels (and we
could take it off by mistake), so a bot reaction will instead serve as a
marker for any later runs.
So if we're applying multiple labels or changing our mind about, say, an
area label after the notification has already been sent, this should
work fine now, without duplicate notifications.
Release Notes:
- N/A
update_git_repositories mapped a changed .git path to a repository with
find_map, so when several repositories share a git directory - a main
checkout plus one of its linked worktrees in the same project worktree -
a ref update under the shared common dir only bumped git_dir_scan_id on
whichever repository iterated first, leaving the others stale until an
unrelated event happened to refresh them. Collect every matching
repository and bump each one.
---
Release Notes:
- N/A or Added/Fixed/Improved ...
GlobalWatcher::add returns Ok(None) while the native watch-limit
cooldown is active, and FsWatcher::add_existing_path treated that as
success: the path was never watched, with no retry and no error. A
long-lived watch (like a repository's git directory) that happened to
register during a cooldown window silently never received events.
Route the skipped registration through the existing pending-path
machinery, which already polls until registration succeeds and emits a
rescan event for the path so that changes missed in the interim are
picked up.
---
Release Notes:
- N/A or Added/Fixed/Improved ...
Reordering worktree roots by drag-and-drop had silently broken: a
worktree-root filter added to `disjoint_entries` for delete-safety
stripped roots before the drop handler could see them, so root-to-root
drops never reached the existing `move_worktree` reorder path.
This PR is the minimal regression fix:
- Move the worktree-root filter out of `disjoint_entries` and into
`disjoint_effective_entries` (used by cut/copy/delete), so drag-and-drop
keeps seeing roots and single-root reorder works again via the existing
`move_worktree` path.
- Filter worktree roots out of `drag_onto`'s copy branch, so holding the
copy modifier over a drag that contains a root no longer returns `None`
from `create_paste_path` and silently cancels the whole copy.
- Add `test_drag_worktree_root_reorders_worktrees` exercising the
drag-onto reorder flow end to end.
The larger feature work (multi-root group reordering, blank-area "send
to end", copy-mode drag feedback, and syncing worktree order to
collaborators) has been split into a separate follow-up PR so this fix
can land quickly. Note that worktree order was intentionally not synced
during collaboration, so that change is discussed separately.
Closes#46699
Release Notes:
- Fixed drag and drop to reorder worktrees
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
# Objective
Allow zed's language model stack to express OpenAI Responses API custom
tools (freeform text-input tools with an optional lark/regex grammar),
so downstream consumers can offer tools like a freeform `apply_patch` to
GPT models.
## Solution
- `LanguageModelRequestTool` now carries a Function-vs-Custom input
variant; `LanguageModelCustomToolFormat` models text/grammar formats.
- `LanguageModelToolUse.input` becomes a typed
`LanguageModelToolUseInput::{Json, Text}`. Serialization is tagged so
persisted Text inputs round-trip losslessly; legacy plain JSON values
still deserialize as `Json`.
- `open_ai` gains the custom tool wire types (tool definition,
`custom_tool_call`/`custom_tool_call_output` input items with
string-or-content-part outputs, output item, and
`custom_tool_call_input` delta/done stream events). The Responses event
mapper accumulates raw text deltas into `ToolUse` events, and history
replay derives custom-vs-function tool results from the matching
`ToolUse` by id.
- All non-OpenAI providers and the Chat Completions path error
explicitly when a request contains custom tools — no silent drops or
empty-schema coercions.
Release Notes:
- N/A
Closes https://github.com/zed-industries/zed/issues/59083
Closes https://github.com/zed-industries/zed/issues/42958
Closes https://github.com/zed-industries/zed/issues/59612
This PR improve selection copy of Agent Panel and Markdown Preview such
that copied text is always valid markdown, following how VSCode handles
it. The partial selection of styled text would not copy broken syntax
like `bold**` or ```inline code` ``` like before.
Now we follow simple rule:
> Selecting any part, no matter from where, copies it as its markdown.
Except when the selection sits entirely inside a single inline code
span, in which case we copy plain text, for terminal and code use cases.
Examples:
This is **bold** text, this is *italic* text, and this is `code` all `in
one` sentence.
- selecting only bold → `**bold**`
- selecting normal text and partial bold → `is is **bo**`
- selecting a single code span completely → `code`
- selecting partial code → `od`
- selecting partial text and code → `` his is `cod` ``
- selecting multiple code spans partially → `` `ode` all `in o` ``
- selecting multiple code spans end to end → `` `code` all `in one` ``
Nested spans, like **bold with `code` inside**:
- partial text in bold → `**ld wi**`
- partial code in bold → ``**`od`**``
- full code in bold → ``**`code`**``
- the whole sentence → ``**bold with `code` inside**``
Links, like [Visit Rust's website](https://rust.org):
- partial link text → `[bsite](https://rust.org)`
- full link text → `[Visit Rust's website](https://rust.org)`
How it works:
Selection boundaries that land inside delimiter syntax (`**`, backticks,
etc.) first snap out so no delimiter is left half-selected. Then any
spans the selection cuts through get their delimiters re-added,
outermost first, so nested styling stays balanced. Only the root blocks
containing the two selection boundaries are inspected, everything in
between is copied right as is, which also keeps this cheap on large
documents.
Release Notes:
- Improved copying selected text in Agent Panel and Markdown Preview.
Partial selections of styled text now copy as well-formed markdown, and
selections within a single inline code span copy as plain text.
BlockMap::sync unwrapped the transform cursor for every edit, assuming
each edit's old.start lands strictly inside the old transform tree. The
companion (split-diff) branch of sync can compose an edit anchored at
the trailing boundary of the old transforms, leaving the cursor past the
end of the tree and aborting the process on the None unwrap. Only bind
the transform when there are rows preceding the edit.
Fixes ZED-9V4.
Closes FR-113
Release Notes:
- N/A or Added/Fixed/Improved ...
Closes#60325, Follow up to #59415
Fixes an issue where launching Zed from the cli (just `zed`, no path)
would not restore the previous window when using
`cli_default_open_behavior: new_window`
Release Notes:
- cli: Fixed an issue where the previous workspace would not be restored
when using `cli_default_open_behavior: new_window` and no path was
provided
# Objective
Hi! This PR updates the Ruby doc to mention 2 language servers
`kanayago` and `fuzzy-ruby-server`.
## Testing
N/A
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
## Showcase
N/A
---
Release Notes:
- N/A
Jump the caret straight to the next/previous block of prose comments —
like paragraph motion, but for comments.
The motivating workflow: reflowing a file's comments with `editor:
rewrap`. Today that means reaching for the mouse to click into each
comment block scattered through the file, or using other cursor motions
and undershooting or overshooting the target. With these two actions you
can step from one comment paragraph to the next, `rewrap`, and repeat —
reflowing every comment in a file without ever touching the mouse. It's
also handy for skimming a heavily-documented file: hop from doc comment
to doc comment without manually scrolling past the code in between.
Adds two editor actions:
- `editor::MoveToNextCommentParagraph`
- `editor::MoveToPreviousCommentParagraph`
Both move the caret to the first non-whitespace character of the
next/previous *comment paragraph*. They have no default keybinding and
are available from the command palette ("editor: move to next/previous
comment paragraph").
### What counts as a comment paragraph
A comment paragraph is a run of consecutive comment lines. A line is a
comment line when its **first non-whitespace character is in a `comment`
syntax scope** and the line contains prose (at least one alphanumeric
character). This is determined from the syntax tree
(`language_scope_at(...).override_name()`), the same mechanism `rewrap`
and comment folding already use, so it behaves correctly without
per-language string matching:
- **End-of-line comments preceded by code are ignored** — on `let x = 1;
// note` the first non-whitespace character is code, not a comment, so
the line is not a paragraph line.
- **`//` inside a string literal is ignored** — its scope is `string`,
not `comment`.
- **Blank/divider comment lines separate paragraphs** — a bare `//` or
`// -----` (no prose) acts as a separator, so you can hop between
paragraphs *within* one comment block as well as across blocks.
Both directions always move to a paragraph *other* than the one the
caret is in: when the caret is inside a paragraph, the whole current
paragraph is skipped, so `Prev` lands on the previous paragraph's start
rather than the current paragraph's own start.
### On the autoscroll
These two actions scroll the destination near the top of the viewport
(`Autoscroll::top_relative`) rather than using the default `Fit`
strategy that sibling motions use. This is deliberate and specific to
the feature: you are jumping to the **start** of a comment paragraph
that extends *downward*, so biasing the destination toward the top keeps
the whole paragraph visible after the jump. This matters for the rewrap
workflow above — you want to see the full comment you are about to
reflow, and the reflow itself changes the paragraph's line count. With
the default `Fit`, repeated forward jumps creep the caret to the bottom
edge and leave long paragraphs cut off below the fold — the opposite of
what this motion is for. Happy to revisit the exact strategy/offset if
you'd prefer consistency with the other motions.
### Tests
Two tests in `editor_tests.rs` (using a real grammar + comment override
query):
- `test_move_to_next_and_previous_comment_paragraph` — full
forward/backward round trip, covering blank comment-line separators,
code separators, trailing comments, and the no-more-paragraphs stop.
- `test_move_to_previous_comment_paragraph_skips_current_paragraph` —
`Prev` from mid-paragraph skips to the previous paragraph, and stays put
when there is no previous paragraph.
---
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments — N/A, no unsafe
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- Added `editor::MoveToNextCommentParagraph` and
`editor::MoveToPreviousCommentParagraph` actions to move the caret
between comment paragraphs
`PolychromeSprite` in `crates/gpui/src/scene.rs` is `#[repr(C)]` and had
a `grayscale: bool` field followed by 3 compiler-inserted padding bytes
that were never written. The wgpu renderer's `instance_bytes`
reinterprets `&[PolychromeSprite]` as `&[u8]` via
`slice::from_raw_parts` and passes it to `queue.write_buffer`, so those
uninitialized padding bytes were exposed behind a shared `&[u8]` on
every frame that draws an image or emoji, which is undefined behavior.
Rather than widening the field to a raw `u32` (which would suggest
values other than 0 and 1 are meaningful), this introduces
`PaddedBool32`: a `#[repr(transparent)]` wrapper around `u32` whose only
public constructor is `From<bool>`, so the 0-or-1 invariant is enforced
by the type while the layout has no padding. `Underline.wavy`, which was
already a raw `u32` for the same reason, is converted too.
cbindgen emits the wrapper as `typedef uint32_t PaddedBool32;`, so the
generated Metal header and shaders are unchanged. The WGSL and HLSL
shaders already declared these fields as `u32`/`uint`; their `& 0xFFu`
masks, which existed to ignore the garbage padding bytes, are now
simplified to plain comparisons.
Release Notes:
- N/A
Context
This PR makes the Zed docs easier for AI tools, search crawlers, and
users to consume without changing the visible docs content. The current
production docs are primarily optimized for browser navigation. They do
not expose first-class Markdown URLs, an `llms.txt` index, page-level
copy affordances, or machine-readable freshness metadata that let users
and agents grab clean, current page content.
Changes
- Generate Markdown copies for docs pages during the mdBook postprocess
step, including `/docs/index.md` as an alias for Getting Started.
- Generate `/docs/llms.txt` from the mdBook chapter list, grouped by
`SUMMARY.md` sections and annotated with page frontmatter descriptions.
- Generate `/docs/sitemap.xml` with `<lastmod>` values for every docs
page.
- Emit machine-readable freshness metadata in HTML via `last-modified`
and `article:modified_time` meta tags.
- Generate Cloudflare Pages `_redirects` for `.html`, extensionless, and
`.md` redirect variants, with channel-aware docs destinations.
- Add discovery hints for agents and crawlers: `rel="llms.txt"`,
`rel="alternate" type="text/markdown"`, and a short generated `llms.txt`
directive in copied Markdown pages.
- Update the docs proxy so `Accept: text/markdown`, `/docs.md`, and
direct `.md` requests can resolve to the generated Markdown artifacts.
- Move primary docs content earlier in the HTML source while preserving
the visible layout, so crawlers and agent scorers encounter the article
before sidebar chrome.
- Move the existing copy-as-Markdown control from the top navigation
into the page-title row, using the generated Markdown alternate link as
the source of truth.
- Split AI-discovery artifact generation out of
`docs_preprocessor/src/main.rs` into a focused module.
Best Practices Adopted
- Use `llms.txt` as a concise navigation index, not a dump of full page
content.
- Link to absolute, canonical Markdown URLs from `llms.txt`.
- Preserve the docs hierarchy in `llms.txt` instead of emitting a flat
sitemap-like list.
- Include short per-link descriptions from existing metadata rather than
inventing summaries.
- Keep `llms.txt`, Markdown copies, sitemap data, redirects, and
freshness metadata generated from the same mdBook source to avoid drift.
- Advertise Markdown alternates with standard HTML metadata and
same-origin URLs.
- Support both explicit Markdown URLs and content negotiation for
clients that prefer Markdown.
- Keep browser copy behavior pointed at generated Markdown alternate
links instead of duplicating route inference in JavaScript.
- Keep the copy-as-Markdown affordance in the page title row without
duplicating header chrome controls.
Validation
- `cargo check -p docs_preprocessor`
- `cargo test -p docs_preprocessor`
- `./script/clippy -p docs_preprocessor`
- `mdbook build ./docs --dest-dir=../target/deploy/docs/`
- `node --check docs/theme/plugins.js`
- `pnpm dlx prettier@3.5.0 docs/theme/plugins.js --check`
- `git diff --check`
- Local artifact checks confirmed generated Markdown pages, `llms.txt`,
`sitemap.xml` lastmod values, HTML freshness metadata, Markdown
alternate links, redirect targets, and preprocessed action/keybinding
tags resolve as expected.
- Worker URL rewrite mock covered `/docs/`, `/docs.md`,
`/docs/index.md`, extensionless docs routes, `.html` routes,
`/docs/llms.txt`, and `/docs/sitemap.xml`.
- High-effort adversarial subagent review found blockers around
channel-aware redirects, shallow-checkout date fallback, file size,
duplicated Markdown path inference, and process spawning. Those were
addressed.
Remaining Notes
- Local `python -m http.server` does not emulate Cloudflare Pages pretty
URLs, `_redirects`, or the docs-proxy Worker, so full local `afdocs`
still cannot prove content negotiation end to end.
- Existing production remains unchanged until this PR is deployed
through the docs workflow.
- Production baseline `npx afdocs check https://zed.dev/docs/ --fixes
--verbose` still reports the original failures before this PR is
deployed: 12 passed, 8 failed, 3 skipped.
Release Notes:
- Improved docs AI-readiness by adding machine-readable discovery,
Markdown access, and freshness metadata.
---------
Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
On Wayland, closing a window in the brief gap between requesting a
portal file dialog and ashpd exporting the window's surface (used to
parent the dialog) crashed Zed with `Unknown opcode 0 for object
<anonymous>@0`
([ZED-9KB](https://zed-dev.sentry.io/issues/7568720776/)). When the
export request fails because the surface is already dead,
wayland-scanner's generated code silently returns an inert proxy, and
ashpd's `Drop` impl later sends `destroy` on it — which wayland-backend
0.3.11 answers with a panic, because it looks up the request opcode
before checking whether the object is null. wayland-backend 0.3.15 fixes
this
([Smithay/wayland-rs#890](https://github.com/Smithay/wayland-rs/issues/890))
by returning an error instead, which the generated destructor discards,
so the drop becomes a harmless no-op. This bumps the lockfile to 0.3.15
and raises the version floor in `gpui_linux` so the fix can't silently
regress via a fresh lockfile.
Closes FR-100
Release Notes:
- Fixed a crash on Linux (Wayland) when a window was closed just as a
file dialog was being opened.
# Objective
Fix two gaps in element hover tracking at window boundaries. Hover was
only re-evaluated on `MouseMove`, so when the pointer left the window no
event fired `on_hover(false)` and the element stayed hovered.
Symmetrically on Wayland, no `Motion` follows `Enter` until the pointer
moves again, so hover was not established at the entry pixel. Both cases
are easy to miss since most hover-styled elements don't sit flush
against the window edge, but they surfaced while implementing
layer_shell popups with input_regions, which should close when stop
hovering.
## Solution
The hover compare-and-fire logic in `div` is refactored into a shared
`update_hover` closure, and a second listener on `MouseExitEvent` clears
hover when the pointer leaves the window. It clears unconditionally
because `MouseExited` doesn't update the tracked mouse position, so a
hit test during that dispatch would still report the element as hovered.
On Wayland, a `MouseMove` is synthesized at the entry position on
`wl_pointer.enter`, mirroring the `MouseExited` already dispatched on
`Leave`.
## Testing
Tested manually on Wayland/Linux: hover on a window-edge element clears
when the pointer leaves the window, and hover is established immediately
when the pointer enters a surface with an element under the entry pixel.
Not tested on other platforms. The `div` change relies on each
platform's existing `MouseExited` dispatch: macOS and X11 emit it, so
they get the exit fix too. Windows never dispatches `MouseExited`
(`WM_MOUSELEAVE` only flips the window-level hover flag), so the
stuck-hover case might remain there, unchanged from before.
Before:
https://github.com/user-attachments/assets/6af83bb3-de9d-40e2-a64c-bdefc98fc96d
After:
https://github.com/user-attachments/assets/721a9b5c-108a-499f-867e-da111836a34a
Here is an example application to test this:
```rs
#![cfg_attr(target_family = "wasm", no_main)]
use gpui::{
App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
};
use gpui_platform::application;
struct HoverExit {
hovered: bool,
}
impl Render for HoverExit {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Fills the whole window so its edge is the window edge: moving the mouse
// out of the window is what exercises the MouseExited path.
div()
.id("hover-exit")
.size_full()
.flex()
.justify_center()
.items_center()
.text_xl()
.text_color(rgb(0xffffff))
.bg(if self.hovered {
rgb(0x585f58)
} else {
rgb(0x505050)
})
.child(if self.hovered { "HOVERED" } else { "not hovered" })
.on_hover(cx.listener(|this, hovered, _, cx| {
this.hovered = *hovered;
cx.notify();
}))
}
}
fn run_example() {
application().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(240.), px(160.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
app_id: Some("gpui-hover-exit".to_string()),
..Default::default()
},
|_, cx| cx.new(|_| HoverExit { hovered: false }),
)
.unwrap();
cx.activate(true);
});
}
#[cfg(not(target_family = "wasm"))]
fn main() {
run_example();
}
#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
gpui_platform::web_init();
run_example();
}
```
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- Fixed element hover state not clearing when the mouse leaves the
window
Checks that SetWindowCompositionAttribute was found before transmuting
and calling the function pointer, making the composition setup a no-op
when the undocumented export is unavailable.
Release Notes:
- N/A
# Objective
gpui can't show UI that extends past the window it belongs to. Menus,
dropdowns and tooltips are drawn as elements inside the window, so they
clip at its edges. This PR adds a window kind for platform-native popups
anchored to a parent window, as groundwork for real native menus,
dropdowns and tooltips.
## Solution
`WindowKind::AnchoredPopup(PopupOptions)` opens a popup positioned
relative to a parent window. Instead of giving the popup an absolute
position, you describe where it should go and the platform figures out
the rest:
- `parent`: the window to anchor to
- `anchor_rect`: a rectangle in the parent, e.g. the button that opened
the menu
- `anchor` and `gravity`: which point of that rect to attach to, and
which direction to grow
- `constraint_adjustment`: what the platform may do if the popup would
leave the screen (slide, flip, resize)
- `grab`: menu behavior, the popup takes focus and is dismissed when
clicking outside the app
The popup's size comes from `WindowOptions::window_bounds`.
This model mirrors Wayland's `xdg_positioner`, where the compositor owns
positioning and the client can only describe intent. Since that's the
most restrictive case, the other platforms can implement the same
description later with simple math against screen bounds.
Only Wayland is implemented so far, via `xdg_popup` on top of the
existing surface implementation. Popups can be parented to toplevels,
layer-shell surfaces (a menu opened from a panel) and other popups
(nested menus). macOS, Windows, X11 and web reject the kind with
`PopupNotSupportedError`, so callers can detect that and fall back to
in-window popovers.
Some Wayland details that might help during review:
- Anchor rects are translated from gpui coordinates into the parent's
window geometry space and clamped to it. A rect outside the geometry, or
with zero size, is a fatal protocol error
- Resizing a mapped popup goes through `xdg_popup.reposition`
- Mouse press serials are now recorded on press only, not release.
Compositors decline grabs and interactive moves that reference a release
serial
## Testing
Tested manually on Wayland with an example app: the menu opens anchored
below its button, extends past the parent window, flips above the button
near the bottom of the screen, and a grabbing popup is dismissed when
clicking into another application.
Nested menus were tested in one of my projects (ignore that they are
ugly, that's just a prototype 😛):
https://github.com/user-attachments/assets/2cd3e2e9-87f7-4b02-986f-48e5633e205c
I also have a complete runnable example demonstrating it. I did not add
it to the PR, because this might give the impression that
`WindowKind::AnchoredPopup` are a complete implementation, despite only
working on wayland so far:
<details>
<summary>Click to view example</summary>
```rust
//! Example and manual test for platform-native popups (`WindowKind::AnchoredPopup`).
//!
//! A native popup is a real, parent-anchored window that can extend beyond its parent onto the
//! screen, unlike gpui's in-window popovers. Run it, open the menu, and confirm the points listed
//! in the window. On a platform without an implementation the button reports that popups are not
//! supported instead of opening anything.
//!
//! Run with: cargo run -p gpui --example popup
#![cfg_attr(target_family = "wasm", no_main)]
use gpui::{
AnyWindowHandle, App, Bounds, Context, MouseButton, SharedString, Window, WindowBounds,
WindowHandle, WindowKind, WindowOptions, div, point, popup::*, prelude::*, px, rgb, size,
};
use gpui_platform::application;
/// The trigger button, at a fixed position so the popup can anchor to a known rectangle. Real code
/// would anchor to the measured bounds of whatever element opens the popup.
const BUTTON_BOUNDS: Bounds<gpui::Pixels> = Bounds {
origin: point(px(24.), px(24.)),
size: size(px(200.), px(32.)),
};
const POPUP_SIZE: gpui::Size<gpui::Pixels> = size(px(260.), px(320.));
struct Menu;
impl Render for Menu {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let item = |label: &str| {
div()
.id(label.to_string())
.px_3()
.py_1()
.rounded_sm()
.hover(|this| this.bg(rgb(0x3a3a3a)))
.cursor_pointer()
.child(label.to_string())
.on_click(|_, window, _| window.remove_window())
};
div()
.id("menu-root")
.size_full()
.p_1()
.flex()
.flex_col()
.gap_0p5()
.bg(rgb(0x2a2a2a))
.text_color(gpui::white())
.rounded_md()
.border_1()
.border_color(rgb(0x454545))
.child(item("Foo"))
.child(item("Bar"))
.child(item("Baz"))
.child(item("Qux"))
.child(item("Alice"))
.child(item("Bob"))
}
}
struct PopupExample {
menu: Option<WindowHandle<Menu>>,
status: SharedString,
}
impl Default for PopupExample {
fn default() -> Self {
Self {
menu: None,
status: "Click \"Open menu\" to open a native popup.".into(),
}
}
}
impl PopupExample {
/// Closes the menu if it is open. Returns true if a menu was actually open.
fn close_menu(&mut self, cx: &mut App) -> bool {
match self.menu.take() {
Some(menu) => menu
.update(cx, |_, window, _| window.remove_window())
.is_ok(),
None => false,
}
}
fn toggle_menu(&mut self, parent: AnyWindowHandle, cx: &mut App) {
if self.close_menu(cx) {
return;
}
match open_menu(parent, cx) {
Ok(menu) => {
self.menu = Some(menu);
self.status = "Menu open. Dismiss it by selecting an item, clicking elsewhere in \
this window, or clicking another application."
.into();
}
// A real application would fall back to an in-window popover here.
Err(error) => {
self.status = format!("Failed to open a native popup: {error}").into();
log::error!("failed to open popup: {error}");
}
}
}
}
impl Render for PopupExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let bullet = |text: &str| div().child(format!("• {text}"));
div()
.id("root")
.size_full()
.bg(rgb(0xf7f7f7))
.text_color(rgb(0x222222))
// Same-app clicks don't auto-dismiss a grabbing popup (see `PopupOptions::grab`).
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, _window, cx| {
this.close_menu(cx);
}),
)
.child(
div()
.size_full()
.p_5()
.pt(px(76.))
.flex()
.flex_col()
.gap_3()
.child(div().text_xl().child("Native popup test"))
.child(div().text_sm().child(
"WindowKind::AnchoredPopup opens a real, parent-anchored window that can \
extend past this window onto the screen. Only some platforms implement \
it so far.",
))
.child(
div()
.flex()
.flex_col()
.gap_1()
.text_sm()
.text_color(rgb(0x555555))
.child(div().child("Verify:"))
.child(bullet("The menu opens anchored below the button."))
.child(bullet(
"The menu extends past the bottom edge of this window.",
))
.child(bullet(
"Near the bottom of the screen, the menu flips above the button.",
))
.child(bullet("Clicking another application dismisses the menu."))
.child(bullet(
"Selecting an item or clicking in this window dismisses it.",
)),
)
.child(
div()
.text_sm()
.text_color(rgb(0x333333))
.child(self.status.clone()),
),
)
.child(
div()
.absolute()
.left(BUTTON_BOUNDS.origin.x)
.top(BUTTON_BOUNDS.origin.y)
.w(BUTTON_BOUNDS.size.width)
.h(BUTTON_BOUNDS.size.height)
.flex()
.items_center()
.justify_center()
.bg(rgb(0xffffff))
.border_1()
.border_color(rgb(0xd0d0d0))
.rounded_md()
.cursor_pointer()
.id("open-menu")
.active(|this| this.bg(rgb(0xeeeeee)))
.child("Open menu ▾")
// Open on mouse-down, not on click, so the grab is taken while the button is still held.
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
// Don't let the window handler above close the menu we are opening.
cx.stop_propagation();
this.toggle_menu(window.window_handle(), cx);
}),
),
)
}
}
fn open_menu(parent: AnyWindowHandle, cx: &mut App) -> anyhow::Result<WindowHandle<Menu>> {
cx.open_window(
WindowOptions {
titlebar: None,
// Sizes the popup. The platform decides the position, so the origin is ignored.
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: point(px(0.), px(0.)),
size: POPUP_SIZE,
})),
kind: WindowKind::AnchoredPopup(PopupOptions {
parent,
anchor_rect: BUTTON_BOUNDS,
// Anchor to the button's bottom-left and grow down-right so the menu drops beneath it.
anchor: PopupAnchor::BottomLeft,
gravity: PopupGravity::BottomRight,
// Slide horizontally and flip vertically if the menu would leave the screen.
constraint_adjustment: PopupConstraintAdjustment::SLIDE_X
| PopupConstraintAdjustment::FLIP_Y,
offset: point(px(0.), px(4.)),
// Grab input so the compositor dismisses the popup on clicks into other applications.
grab: true,
}),
..Default::default()
},
|_, cx| cx.new(|_| Menu),
)
}
fn run_example() {
application().run(|cx: &mut App| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: point(px(100.), px(100.)),
size: size(px(420.), px(300.)),
})),
..Default::default()
},
|_, cx| cx.new(|_| PopupExample::default()),
)
.unwrap();
cx.activate(true);
});
}
#[cfg(not(target_family = "wasm"))]
fn main() {
run_example();
}
#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
gpui_platform::web_init();
run_example();
}
```
</details>
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- N/A
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes#43396
Release Notes:
- Project search now supports CRLF line endings correctly, as well as
other regex features like subroutine calls
## Summary
Introduces `format_on_save` variants that format only modified lines,
limiting formatting to lines changed since the last commit. This
prevents massive diffs when editing legacy codebases. Aligns with VS
Code's `editor.formatOnSaveMode` naming:
- `"modifications"` — formats only git-diffed lines. Requires source
control; skips formatting if no diff is available.
- `"modifications_if_available"` — formats only git-diffed lines,
falling back to full-file formatting for untracked files, when source
control is unavailable, or when the LSP does not support range
formatting.
Also supports importing equivalent VS Code settings
(`formatOnSaveMode`).
This PR uses the range-based whitespace/newline infrastructure from the
dependency PR above.
## Changes
<details>
<summary><b>New settings, modified ranges computation, VS Code
import</b></summary>
### New settings (`language.rs`, `default.json`, `all-settings.md`)
Two new `FormatOnSave` variants: `Modifications` and
`ModificationsIfAvailable`.
### Modified ranges computation (`items.rs`)
- `compute_format_decision()` — reads `format_on_save` setting across
all buffers, determines whether to use ranged formatting (`Ranges`),
full formatting (`Full`), or skip (`Skip`).
- `compute_modified_ranges()` — extracts modified line ranges from git
diff hunks via `BufferDiffSnapshot`.
- `is_empty_range()` — helper to detect deletion-only hunks that produce
no formatable content.
The `save()` method calls `compute_format_decision()` and dispatches
accordingly.
### Modifications mode in `lsp_store.rs`
- Adds `FormatOnSave::Modifications |
FormatOnSave::ModificationsIfAvailable` to the formatter selection match
arm.
- `ModificationsIfAvailable` falls back to full-file formatting when
ranged formatting produces no results.
### VS Code settings import (`vscode_import.rs`, `settings_store.rs`)
Imports `editor.formatOnSaveMode` mapping:
- `"modifications"` → `FormatOnSave::Modifications`
- `"modificationsIfAvailable"` →
`FormatOnSave::ModificationsIfAvailable`
- `"file"` → `FormatOnSave::On`
</details>
## Tests
- `test_modifications_format_on_save` — basic modifications mode
formatting with dirty buffer
- `test_modifications_format_no_changes` — clean buffer triggers no
formatting
- `test_modifications_format_lsp_no_range_support` — LSP without range
formatting skips entirely for `Modifications`
- `test_modifications_format_lsp_returns_empty_edits` — empty edits
handled gracefully
- `test_modifications_format_multiple_hunks` — two non-adjacent edits
produce two separate range formatting requests
---
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes#16509
Depends on #53942
Release Notes:
- Added `modifications` and `modifications_if_available` options to
`format_on_save`, which format only git-changed lines instead of the
entire file
- When using modifications mode, `remove_trailing_whitespace_on_save`
and `ensure_final_newline_on_save` are also scoped to changed lines,
preventing unwanted diff jitter in legacy codebases
- Added support for importing VS Code's `editor.formatOnSaveMode`
setting
---------
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
## Context
This is an implementation of this feature:
https://github.com/zed-industries/zed/discussions/49821
Although, I'd argue it's a bug fix, not a feature.
Either way : This copies the window scroll logic from the `vim` mode
versions with out all the extra logic for visual mode.
Could refactor the common logic out of the vim code and make it common.
But that seems like a bigger PR. Happy to take a stab at it if that's
what you prefer.
Could also add new commands for the new behavior if you prefer. I didn't
do that, because it seems like more clutter in the commands, and my
belief that the existing behavior is a bug. But happy to do that if you
prefer.
## How to Review
creates new function `scroll_screen_with_cursor_margin` in `scroll.rs`
wires up editor::LineUp/LineDown to new function in `elements.rs`
adds a test in `editor_tests.rs`
## Self-Review Checklist
<!-- Check before requesting review: -->
- [X] I've reviewed my own diff for quality, security, and reliability
- [X] Unsafe blocks (if any) have justifying comments
- [X] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [X] Tests cover the new/changed behavior
- [X] Performance impact has been considered and is acceptable
Release Notes:
- editor::LineUp/LineDown commands now honor vertical_scroll_margin
(same as vim::LineUp/LineDown)
---------
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes#55792
Release Notes:
- Fixed files in pnpm workspaces moving to symlinked `node_modules`
paths after saving.
---------
Co-authored-by: Kirill Bulatov <kirill@zed.dev>