Compare commits

..

42 Commits

Author SHA1 Message Date
Nathan Sobo
c8b363f4b0 gpui: Expose ShapedLine::width() for pen advancement 2025-12-25 07:47:36 -07:00
Nathan Sobo
47df9d22a0 chore: update generated cargo manifests 2025-12-25 07:47:36 -07:00
Nathan Sobo
93ebd240d4 Merge branch 'ex-pointer-capture' into ex 2025-12-23 12:47:02 -07:00
Nathan Sobo
c7d55b243b Add pointer capture API for stable drag handling
Add minimal pointer capture API to gpui::Window:
- capture_pointer(hitbox_id): starts capture for the given hitbox
- release_pointer(): releases capture
- captured_hitbox(): returns the captured hitbox, if any

When captured, HitboxId::is_hovered() returns true for the captured
hitbox regardless of actual hit testing. Capture is automatically
released on MouseUpEvent.

This enables drag operations (like scrollbar thumb dragging) to
continue working even when the pointer moves outside the element's
bounds during the drag.
2025-12-23 12:46:56 -07:00
Nathan Sobo
d49a8e04e6 Add pointer capture API for stable drag handling
Add minimal pointer capture API to gpui::Window:
- capture_pointer(hitbox_id): starts capture for the given hitbox
- release_pointer(): releases capture
- captured_hitbox(): returns the captured hitbox, if any

When captured, HitboxId::is_hovered() returns true for the captured
hitbox regardless of actual hit testing. Capture is automatically
released on MouseUpEvent.

This enables drag operations (like scrollbar thumb dragging) to
continue working even when the pointer moves outside the element's
bounds during the drag.
2025-12-23 12:30:02 -07:00
Nathan Sobo
1f34525634 Merge origin/main into ex-local 2025-12-18 09:07:13 -07:00
Sean Hagstrom
2d071b0cb6 editor: Fix git-hunk toggling for adjacent hunks (#43187)
Closes #42934 

Release Notes:

- Fix toggling adjacent git-diff hunks based on the reported behaviour
in #42934

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-12-18 16:45:55 +01:00
Alvaro Parker
bd2b0de231 gpui: Add modal dialog window kind (#40291)
Closes #ISSUE

A [modal dialog](https://en.wikipedia.org/wiki/Modal_window) window is a
window that demands the user's immediate attention and blocks
interaction with other parts of the application until it's closed.

- On Windows this is done by disabling the parent window when the dialog
window is created and re-enabling the parent window when closed.
- On Wayland this is done using the
[`XdgDialog`](https://wayland.app/protocols/xdg-dialog-v1) protocol,
which hints to the compositor that the dialog should be modal. While
compositors like GNOME and KDE block parent interaction automatically,
the XDG specification does not guarantee this behavior, compositors may
deliver events to the parent window unfiltered. Since the specification
explicitly requires clients to implement event filtering logic
themselves, this PR implements client-side blocking in GPUI to ensure
consistent modal behavior across all Wayland compositors, including
those like Hyprland that don't block parent interaction.
- On X11 this is done by enabling the application window property
[`_NET_WM_STATE_MODAL`](https://specifications.freedesktop.org/wm/latest/ar01s05.html#id-1.6.8)
state.

I'm unable to implement this on MacOS as I lack the experience and the
hardware to test it. If anyone is interested on implementing this let me
know.

|Window|Linux (wayland)| Linux (x11) |MacOS|
|-|-|-|-|
|<video
src="https://github.com/user-attachments/assets/bfd0733a-445d-4b63-ac6b-ebe098a7dc74"></video>|<video
src="https://github.com/user-attachments/assets/024cd6ec-ff81-4250-a5be-5d207a023f8c"></video>|
N/A | <video
src="https://github.com/user-attachments/assets/656e60a5-26b2-4ee2-8368-1fbbe872453c"></video>|

TODO:

- [x] Block parent interaction client-side on X11

Release Notes:

- Added modal dialog window kind on GPUI

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
Co-authored-by: Anthony Eid <anthony@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-12-18 16:45:06 +01:00
Bennet Bo Fenner
886de8f54b agent_ui: Improve UX when pasting code into message editor (#45254)
Follow up to #42982

Release Notes:

- agent: Allow pasting code without formatting via ctrl/cmd-shift-v.
- agent: Fixed an issue where pasting a single line of code would always
insert an @mention
2025-12-18 16:38:47 +01:00
Ben Brandt
7a783a91cc acp: Update to agent-client-protocol rust sdk v0.9.2 (#45255)
Release Notes:

- N/A
2025-12-18 15:01:20 +00:00
Ahmed M. Ammar
f9462da2f7 terminal: Fix pane re-entrancy panic when splitting terminal tabs (#45231)
## Summary
Fix panic "cannot update workspace::pane::Pane while it is already being
updated" when dragging terminal tabs to split the pane.

## Problem
When dragging a terminal tab to create a split, the app panics due to
re-entrancy: the drop handler calls `terminal_panel.center.split()`
synchronously, which invokes `mark_positions()` that tries to update all
panes in the group. When the pane being updated is part of the terminal
panel's center group, this causes a re-entrancy panic.

## Solution
Defer the split operation using `cx.spawn_in()`, similar to how
`move_item` was already deferred in the same handler. This ensures the
split (and subsequent `mark_positions()` call) runs after the current
pane update completes.

## Test plan
- Open terminal panel
- Create a terminal tab
- Drag the terminal tab to split the pane
- Verify no panic occurs and split works correctly
2025-12-18 14:34:33 +00:00
Danilo Leal
61dd6a8f31 agent_ui: Add some fixes to tool calling display (#45252)
- Follow up to https://github.com/zed-industries/zed/pull/45097 — not
showing raw inputs for edit and terminal calls
- Removing the display of empty Markdown if the model outputs it

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-12-18 14:34:10 +00:00
Ben Brandt
abb199c85e thread_view: Clearer authentication states (#45230)
Closes #44717

Sometimes, we show the user the agent's auth methods because we got an
AuthRequired error.

However, there are also several ways a user can choose to re-enter the
authentication flow even though they are still logged in.

This has caused some confusion with several users, where after logging
in, they type /login again to see if anything changed, and they saw an
"Authentication Required" warning.

So, I made a distinction in the UI if we go to this flow from a concrete
error, or if not, made the language less error-like to help avoid
confusion.

| Before | After |
|--------|--------|
| <img width="1154" height="446" alt="Screenshot 2025-12-18 at 10 
54@2x"
src="https://github.com/user-attachments/assets/9df0d59a-2d45-4bfc-ba85-359dd1a4c8ae"
/> | <img width="1154" height="446" alt="Screenshot 2025-12-18 at 10 
53@2x"
src="https://github.com/user-attachments/assets/73a9fb45-4e6f-4594-8795-aaade35b2a72"
/> |


Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Miguel Raz Guzmán Macedo <miguel@zed.dev>
2025-12-18 14:03:11 +00:00
Lukas Wirth
cebbf77491 gpui(windows): Fix clicks to inactive windows not dispatching to the clicked window (#45237)
Release Notes:

- Fixed an issue on windows where clicking buttons on windows in the
background would not register as being clicked on that window
2025-12-18 13:05:20 +00:00
Ben Brandt
0180f3e72a deepseek: Fix for max output tokens blocking completions (#45236)
They count the requested max_output_tokens against the prompt total.
Seems like a bug on their end as most other providers don't do this, but
now we just default to None for the main models and let the API use its
default behavior which works just fine.

Closes: #45134

Release Notes:

- deepseek: Fix issue with Deepseek API that was causing the token limit
to be reached sooner than necessary
2025-12-18 12:47:34 +00:00
rabsef-bicrym
5488a19221 terminal: Respect RevealStrategy::NoFocus and Never focus settings (#45180)
Closes #45179

## Summary

Fixes the focus behavior when creating terminals with
`RevealStrategy::NoFocus` or `RevealStrategy::Never`. Previously,
terminals would still receive focus if the terminal pane already had
focus, contradicting the documented behavior.

## Changes

- **`add_terminal_task()`**: Changed focus logic to only focus when
`RevealStrategy::Always`
- **`add_terminal_shell()`**: Same fix

The fix changes:
```rust
// Before
let focus = pane.has_focus(window, cx)
    || matches!(reveal_strategy, RevealStrategy::Always);

// After  
let focus = matches!(reveal_strategy, RevealStrategy::Always);
```

## Impact

This affects:
- Vim users running `:!command` (uses `NoFocus`)
- Debugger terminal spawning (uses `NoFocus`)
- Any programmatic terminal creation requesting background behavior

Release Notes:

- Fixed terminal focus behavior to respect `RevealStrategy::NoFocus` and
`RevealStrategy::Never` settings when the terminal pane already has
focus.
2025-12-18 12:11:14 +00:00
Henry Chu
bb1198e7d6 languages: Allow using locally installed ty for Python (#45193)
Release Notes:

- Allow using locally installed `ty` for Python
2025-12-18 12:54:34 +01:00
Kirill Bulatov
69fe27f45e Keep tab stop-less snippets in completion list (#45227)
Closes https://github.com/zed-industries/zed/issues/45083

cc @agu-z 

Release Notes:

- Fixed certain rust-analyzer snippets not shown
2025-12-18 11:29:41 +00:00
Lukas Wirth
469da2fd07 gpui: Fix Windows credential lookup returning error instead of None when credentials don't exist (#45228)
This spams the log with amazon bedrock otherwise

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-18 11:23:11 +00:00
shibang
4f87822133 gpui: Persist window bounds and display when detaching a workspace session (#45201)
Closes #41246 #45092

Release Notes:

- N/A

**Root Cause**:
Empty local workspaces returned `DetachFromSession` from 
`serialize_workspace_location()`, and the `DetachFromSession` handler
only cleared the session_id **without saving window bounds**.

**Fix Applied**:
Modified the `DetachFromSession` handler to save window bounds via
`set_window_open_status()`:
```rust
WorkspaceLocation::DetachFromSession => {
    let window_bounds = SerializedWindowBounds(window.window_bounds());
    let display = window.display(cx).and_then(|d| d.uuid().ok());
    window.spawn(cx, async move |_| {
        persistence::DB
            .set_window_open_status(database_id, window_bounds, display.unwrap_or_default())
            .await.log_err();
        persistence::DB.set_session_id(database_id, None).await.log_err();
    })
}
```

**Recording**:


https://github.com/user-attachments/assets/2b6564d4-4e1b-40fe-943b-147296340aa7
2025-12-18 12:03:42 +01:00
Ben Brandt
9a69d89f88 thread_view: Remove unused acp auth method (#45221)
This was from an early iteration and this code path isn't used anymore

Release Notes:

- N/A
2025-12-18 10:47:36 +00:00
Kirill Bulatov
54f360ace1 Add a test to ensure we invalidate brackets not only on edits (#45219)
Follow-up of https://github.com/zed-industries/zed/pull/45187

Release Notes:

- N/A
2025-12-18 10:42:37 +00:00
Ben Brandt
b2a0b78ece acp: Change default for gemini back to managed version (#45218)
It seems we unintentionally changed the default behavior of if we use
the gemini on the path in #40663

Changing this back so by default we use a managed version of the CLI so
we can better control min versions and the like, but still allow people
to override if they need to.

Release Notes:

- N/A
2025-12-18 10:25:06 +00:00
MostlyK
f1ca2f9f31 workspace: Fix new projects opening with default window size (#45204)
Previously, when opening a new project (one that was never opened
before), the window bounds restoration logic would fall through to
GPUI's default window sizing instead of using the last known window
bounds.

This change consolidates the window bounds restoration logic so that
both empty workspaces and new projects use the stored default window
bounds, making the behavior consistent: any new window will use the last
resized window's size and position.

Closes #45092 

Release Notes:

- Fixed new files and projects opening with default window size instead
of the last used window size.
2025-12-18 09:57:21 +00:00
Guilherme do Amaral Alves
4b34adedd2 Update Mistral models context length to their recommended values (#45194)
I noticed some of mistral models context lenghts were outdated, they
were updated accordingly to mistral documentation.

The following models had their context lenght changed:

[mistral-large-latest](https://docs.mistral.ai/models/mistral-large-3-25-12)

[magistral-medium-latest](https://docs.mistral.ai/models/magistral-medium-1-2-25-09)

[magistral-small-latest](https://docs.mistral.ai/models/magistral-small-1-2-25-09)

[devstral-medium-latest](https://docs.mistral.ai/models/devstral-2-25-12)

[devstral-small-latest](https://docs.mistral.ai/models/devstral-small-2-25-12)
2025-12-18 09:49:32 +00:00
Oleksii Orlenko
df48294caa agent_ui: Remove unnecessary Arc allocation (#45172)
Follow up to https://github.com/zed-industries/zed/pull/44297.

Initial implementation in ce884443f1 used
`Arc` to store the reference to the hash map inside the iterator while
keeping the lifetime static. The code was later simplified in
5151b22e2e to build the list eagerly but
the Arc was forgotten, although it became unnecessary.

cc @bennetbo

Release Notes:

- N/A
2025-12-18 10:48:45 +01:00
Kirill Bulatov
cdc5cc348f Return back the eager snapshot update (#45210)
Based on
https://github.com/zed-industries/zed/pull/45187#discussion_r2630140112

Release Notes:

- N/A

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-12-18 09:32:35 +00:00
Kirill Bulatov
0f7f540138 Always invalidate tree-sitter data on buffer reparse end (#45187)
Also do not eagerly invalidate this data on buffer reparse start

Closes https://github.com/zed-industries/zed/issues/45182

Release Notes:

- Fixed bracket colorization not applied on initial file open
2025-12-18 02:37:26 +00:00
Kunall Banerjee
184001b33b docs: Add note about conflicting global macOS shortcut (#45186)
This is already noted in our `default-macos.json`, but was never
surfaced in our docs for some reason. A user noted their LSP completions
were not working because they were not aware of the conflicting global
shortcut.

Ref:
https://github.com/zed-industries/zed/issues/44970#issuecomment-3664118523

Release Notes:

- N/A
2025-12-18 02:13:59 +00:00
Xiaobo Liu
225a2a8a20 google_ai: Refactor token count methods in Google AI (#45184)
The change simplifies the `max_token_count` and `max_output_tokens`
methods by grouping Gemini models with identical token limits.

Release Notes:

- N/A
2025-12-17 20:12:40 -06:00
Kirill Bulatov
ea37057814 Restore generic modal closing on mouse click (#45183)
Was removed in
https://github.com/zed-industries/zed/pull/44887/changes#diff-1de872be76a27a9d574a0b0acec4581797446e60743d23b3e7a5f15088fa7e61

Release Notes:

- (Preview only) Fixed certain modals not being dismissed on mouse click
outside
2025-12-18 01:56:12 +00:00
Conrad Irwin
77cdef3596 Attempt to fix the autofix auto scheduler (#45178)
Release Notes:

- N/A
2025-12-18 01:04:12 +00:00
Torstein Sørnes
05108c50fd agent_ui: Make tool call raw input visible (#45097)
<img width="500" height="1246" alt="Screenshot 2025-12-17 at 9  28@2x"
src="https://github.com/user-attachments/assets/eddb290d-d4d0-4ab8-94b3-bcc50ad07157"
/>

Release Notes:

- agent: Made tool calls' raw input visible in the agent UI.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-18 00:34:31 +00:00
Ben Kunkle
07538ff08e Make sweep and mercury API tokens use cx.global instead of OnceLock (#45176)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-18 00:32:46 +00:00
Cole Miller
9073a2666c Revert "git: Mark entries as pending when staging a files making the staged highlighting more "optimistic"" (#45175)
Reverts zed-industries/zed#43434

This caused a regression because the additional pending hunks don't get
cleared.
2025-12-18 00:28:09 +00:00
Max Brunsfeld
843a35a1a9 extension api: Make server id types constructible, to ease writing tests (#45174)
Currently, extensions cannot have tests that call methods like
`label_for_symbol` and `label_for_completion`, because those methods
take a `LanguageServerId`, and that type is opaque, and cannot be
constructed outside of the `zed_extension_api` crate.

This PR makes it possible to construct those types from strings, so that
it's more straightforward to write unit tests for these LSP adapter
methods.

Release Notes:

- N/A
2025-12-17 16:25:07 -08:00
Conrad Irwin
aff93f2f6c More permissions for autofix (#45170)
Release Notes:

- N/A
2025-12-17 17:05:35 -07:00
Nathan Sobo
da6c2a172c WIP: local changes needed by ex 2025-12-16 09:36:05 -07:00
Nathan Sobo
f2409f2605 Run cargo fmt 2025-12-14 12:21:46 -07:00
Nathan Sobo
ce1c228e6e Rename TestAppWindow to TestWindow, internal TestWindow to TestPlatformWindow
- Public API: TestWindow<V> - the new typed test window wrapper
- Internal: TestPlatformWindow - the platform-level mock window (pub(crate))
2025-12-14 10:48:11 -07:00
Nathan Sobo
96ddbd4e13 Add TestApp and TestAppWindow for cleaner GPUI testing
TestApp provides a simpler alternative to TestAppContext with:
- Automatic effect flushing after updates
- Clean window creation returning typed TestAppWindow<V>
- Scene inspection via SceneSnapshot
- Input simulation helpers

Also adds:
- Background::as_solid() helper in color.rs
- SceneSnapshot for inspecting rendered quads/glyphs in scene.rs
2025-12-14 10:43:35 -07:00
Nathan Sobo
f224d2a923 Add TestApp and TestAppWindow for cleaner GPUI testing
Adds zed/crates/gpui/src/app/test_app.rs with:

- TestApp: test context that auto-runs until parked after updates
- TestAppWindow<V>: window wrapper with input simulation helpers

Minor improvement over TestAppContext/VisualTestContext - mainly
convenience (auto-parking, owned window handle, cleaner signatures).

Does NOT solve the deeper issues:
- Scene is still pub(crate), can't inspect rendered output
- Editor still needs FocusHandle which needs real GPUI context
- TestEditor duplication in ex still exists

3 tests included demonstrating basic usage.
2025-12-14 10:34:21 -07:00
63 changed files with 3498 additions and 1805 deletions

View File

@@ -61,7 +61,8 @@ jobs:
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::cargo_fmt
- id: cargo_fmt
name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: extension_tests::run_clippy

View File

@@ -26,7 +26,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::clippy
- id: clippy
name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
@@ -71,15 +72,15 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::clippy
- id: clippy
name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
- id: record_clippy_failure
name: steps::record_clippy_failure
if: always()
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -93,6 +94,8 @@ jobs:
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
outputs:
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_windows:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -111,7 +114,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::clippy
- id: clippy
name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large

View File

@@ -20,7 +20,8 @@ jobs:
with:
clean: false
fetch-depth: 0
- name: steps::cargo_fmt
- id: cargo_fmt
name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: ./script/clippy
@@ -44,7 +45,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::clippy
- id: clippy
name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large

View File

@@ -74,18 +74,19 @@ jobs:
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: '9'
- name: ./script/prettier
- id: prettier
name: steps::prettier
run: ./script/prettier
shell: bash -euxo pipefail {0}
- name: steps::cargo_fmt
- id: cargo_fmt
name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
- id: record_style_failure
name: steps::record_style_failure
if: always()
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: ./script/check-todos
run: ./script/check-todos
shell: bash -euxo pipefail {0}
@@ -96,6 +97,8 @@ jobs:
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
with:
config: ./typos.toml
outputs:
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_windows:
needs:
@@ -116,7 +119,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::clippy
- id: clippy
name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large
@@ -163,15 +167,15 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::clippy
- id: clippy
name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
- id: record_clippy_failure
name: steps::record_clippy_failure
if: always()
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -185,6 +189,8 @@ jobs:
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
outputs:
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_mac:
needs:
@@ -205,7 +211,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::clippy
- id: clippy
name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
@@ -585,6 +592,24 @@ jobs:
exit $EXIT_CODE
shell: bash -euxo pipefail {0}
call_autofix:
needs:
- check_style
- run_tests_linux
if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-app-token
name: steps::authenticate_as_zippy
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: run_tests::call_autofix::dispatch_autofix
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

32
Cargo.lock generated
View File

@@ -226,9 +226,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.9.0"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -243,9 +243,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol-schema"
version = "0.10.0"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -793,7 +793,7 @@ dependencies = [
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols 0.32.9",
"wayland-protocols",
"zbus",
]
@@ -7370,7 +7370,7 @@ dependencies = [
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols 0.31.2",
"wayland-protocols",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"windows 0.61.3",
@@ -18927,18 +18927,6 @@ dependencies = [
"xcursor",
]
[[package]]
name = "wayland-protocols"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.9"
@@ -18953,14 +18941,14 @@ dependencies = [
[[package]]
name = "wayland-protocols-plasma"
version = "0.2.0"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols 0.31.2",
"wayland-protocols",
"wayland-scanner",
]
@@ -18973,7 +18961,7 @@ dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols 0.32.9",
"wayland-protocols",
"wayland-scanner",
]

View File

@@ -1,723 +1,11 @@
[workspace]
resolver = "2"
members = [
"crates/acp_tools",
"crates/acp_thread",
"crates/action_log",
"crates/activity_indicator",
"crates/agent",
"crates/agent_servers",
"crates/agent_settings",
"crates/agent_ui",
"crates/agent_ui_v2",
"crates/ai_onboarding",
"crates/anthropic",
"crates/askpass",
"crates/assets",
"crates/assistant_text_thread",
"crates/assistant_slash_command",
"crates/assistant_slash_commands",
"crates/audio",
"crates/auto_update",
"crates/auto_update_helper",
"crates/auto_update_ui",
"crates/aws_http_client",
"crates/bedrock",
"crates/breadcrumbs",
"crates/buffer_diff",
"crates/call",
"crates/channel",
"crates/cli",
"crates/client",
"crates/clock",
"crates/cloud_api_client",
"crates/cloud_api_types",
"crates/cloud_llm_client",
"crates/collab",
"crates/collab_ui",
"crates/collections",
"crates/command_palette",
"crates/command_palette_hooks",
"crates/component",
"crates/context_server",
"crates/copilot",
"crates/crashes",
"crates/credentials_provider",
"crates/dap",
"crates/dap_adapters",
"crates/db",
"crates/debug_adapter_extension",
"crates/debugger_tools",
"crates/debugger_ui",
"crates/deepseek",
"crates/denoise",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_types",
"crates/edit_prediction_ui",
"crates/edit_prediction_context",
"crates/editor",
"crates/eval",
"crates/eval_utils",
"crates/explorer_command_injector",
"crates/extension",
"crates/extension_api",
"crates/extension_cli",
"crates/extension_host",
"crates/extensions_ui",
"crates/feature_flags",
"crates/feedback",
"crates/file_finder",
"crates/file_icons",
"crates/fs",
"crates/fs_benchmarks",
"crates/fsevent",
"crates/fuzzy",
"crates/git",
"crates/git_hosting_providers",
"crates/git_ui",
"crates/go_to_line",
"crates/google_ai",
"crates/gpui",
"crates/gpui_macros",
"crates/gpui_tokio",
"crates/html_to_markdown",
"crates/http_client",
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
"crates/inspector_ui",
"crates/install_cli",
"crates/journal",
"crates/json_schema_store",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_models",
"crates/language_onboarding",
"crates/language_selector",
"crates/language_tools",
"crates/languages",
"crates/line_ending_selector",
"crates/livekit_api",
"crates/livekit_client",
"crates/lmstudio",
"crates/lsp",
"crates/markdown",
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/migrator",
"crates/mistral",
"crates/miniprofiler_ui",
"crates/multi_buffer",
"crates/nc",
"crates/net",
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
"crates/onboarding",
"crates/open_ai",
"crates/open_router",
"crates/outline",
"crates/outline_panel",
"crates/panel",
"crates/paths",
"crates/picker",
"crates/prettier",
"crates/project",
"crates/project_benchmarks",
"crates/project_panel",
"crates/project_symbols",
"crates/prompt_store",
"crates/proto",
"crates/recent_projects",
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/scheduler",
"crates/remote",
"crates/remote_server",
"crates/repl",
"crates/reqwest_client",
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rules_library",
"crates/schema_generator",
"crates/search",
"crates/session",
"crates/settings",
"crates/settings_json",
"crates/settings_macros",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
"crates/sqlez",
"crates/sqlez_macros",
"crates/story",
"crates/storybook",
"crates/streaming_diff",
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/codestral",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
"crates/telemetry",
"crates/telemetry_events",
"crates/terminal",
"crates/terminal_view",
"crates/text",
"crates/theme",
"crates/theme_extension",
"crates/theme_importer",
"crates/theme_selector",
"crates/time_format",
"crates/title_bar",
"crates/toolchain_selector",
"crates/ui",
"crates/ui_input",
"crates/ui_macros",
"crates/ui_prompt",
"crates/util",
"crates/util_macros",
"crates/vercel",
"crates/vim",
"crates/vim_mode_setting",
"crates/which_key",
"crates/watch",
"crates/web_search",
"crates/web_search_providers",
"crates/workspace",
"crates/worktree",
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
"crates/zed_env_vars",
"crates/edit_prediction_cli",
"crates/zeta_prompt",
"crates/zlog",
"crates/zlog_settings",
"crates/ztracing",
"crates/ztracing_macro",
#
# Extensions
#
"extensions/glsl",
"extensions/html",
"extensions/proto",
"extensions/slash-commands-example",
"extensions/test-extension",
#
# Tooling
#
"tooling/perf",
"tooling/xtask",
]
default-members = ["crates/zed"]
members = ["crates/askpass", "crates/assets", "crates/clock", "crates/collections", "crates/fs", "crates/fsevent", "crates/git", "crates/gpui", "crates/gpui_macros", "crates/http_client", "crates/http_client_tls", "crates/icons", "crates/media", "crates/migrator", "crates/net", "crates/paths", "crates/proto", "crates/refineable", "crates/release_channel", "crates/reqwest_client", "crates/rope", "crates/scheduler", "crates/settings", "crates/settings_json", "crates/settings_macros", "crates/sum_tree", "crates/text", "crates/theme", "crates/util", "crates/util_macros", "crates/zlog", "crates/ztracing", "crates/ztracing_macro", "tooling/perf"]
[workspace.package]
publish = false
edition = "2024"
[workspace.dependencies]
#
# Workspace member crates
#
acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" }
activity_indicator = { path = "crates/activity_indicator" }
agent_ui = { path = "crates/agent_ui" }
agent_ui_v2 = { path = "crates/agent_ui_v2" }
agent_settings = { path = "crates/agent_settings" }
agent_servers = { path = "crates/agent_servers" }
ai_onboarding = { path = "crates/ai_onboarding" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
assistant_text_thread = { path = "crates/assistant_text_thread" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
auto_update_ui = { path = "crates/auto_update_ui" }
aws_http_client = { path = "crates/aws_http_client" }
bedrock = { path = "crates/bedrock" }
breadcrumbs = { path = "crates/breadcrumbs" }
buffer_diff = { path = "crates/buffer_diff" }
call = { path = "crates/call" }
channel = { path = "crates/channel" }
cli = { path = "crates/cli" }
client = { path = "crates/client" }
clock = { path = "crates/clock" }
cloud_api_client = { path = "crates/cloud_api_client" }
cloud_api_types = { path = "crates/cloud_api_types" }
cloud_llm_client = { path = "crates/cloud_llm_client" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections", version = "0.1.0" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
crashes = { path = "crates/crashes" }
credentials_provider = { path = "crates/credentials_provider" }
crossbeam = "0.8.4"
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
debug_adapter_extension = { path = "crates/debug_adapter_extension" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }
derive_refineable = { path = "crates/refineable/derive_refineable" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
eval_utils = { path = "crates/eval_utils" }
extension = { path = "crates/extension" }
extension_host = { path = "crates/extension_host" }
extensions_ui = { path = "crates/extensions_ui" }
feature_flags = { path = "crates/feature_flags" }
feedback = { path = "crates/feedback" }
file_finder = { path = "crates/file_finder" }
file_icons = { path = "crates/file_icons" }
fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
git = { path = "crates/git" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
edit_prediction_types = { path = "crates/edit_prediction_types" }
edit_prediction_ui = { path = "crates/edit_prediction_ui" }
edit_prediction_context = { path = "crates/edit_prediction_context" }
inspector_ui = { path = "crates/inspector_ui" }
install_cli = { path = "crates/install_cli" }
journal = { path = "crates/journal" }
json_schema_store = { path = "crates/json_schema_store" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
language_onboarding = { path = "crates/language_onboarding" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
line_ending_selector = { path = "crates/line_ending_selector" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
svg_preview = { path = "crates/svg_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" }
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
nc = { path = "crates/nc" }
net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
onboarding = { path = "crates/onboarding" }
open_ai = { path = "crates/open_ai" }
open_router = { path = "crates/open_router", features = ["schemars"] }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
panel = { path = "crates/panel" }
paths = { path = "crates/paths" }
perf = { path = "tooling/perf" }
picker = { path = "crates/picker" }
prettier = { path = "crates/prettier" }
settings_profile_selector = { path = "crates/settings_profile_selector" }
project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
prompt_store = { path = "crates/prompt_store" }
proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
remote = { path = "crates/remote" }
remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rodio = { git = "https://github.com/RustAudio/rodio", rev ="e2074c6c2acf07b57cf717e076bdda7a9ac6e70b", features = ["wav", "playback", "wav_output", "recording"] }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_json = { path = "crates/settings_json" }
settings_macros = { path = "crates/settings_macros" }
settings_ui = { path = "crates/settings_ui" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
snippets_ui = { path = "crates/snippets_ui" }
sqlez = { path = "crates/sqlez" }
sqlez_macros = { path = "crates/sqlez_macros" }
story = { path = "crates/story" }
streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
codestral = { path = "crates/codestral" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
telemetry = { path = "crates/telemetry" }
telemetry_events = { path = "crates/telemetry_events" }
terminal = { path = "crates/terminal" }
terminal_view = { path = "crates/terminal_view" }
text = { path = "crates/text" }
theme = { path = "crates/theme" }
theme_extension = { path = "crates/theme_extension" }
theme_selector = { path = "crates/theme_selector" }
time_format = { path = "crates/time_format" }
title_bar = { path = "crates/title_bar" }
toolchain_selector = { path = "crates/toolchain_selector" }
ui = { path = "crates/ui" }
ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
ui_prompt = { path = "crates/ui_prompt" }
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
vercel = { path = "crates/vercel" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
which_key = { path = "crates/which_key" }
watch = { path = "crates/watch" }
web_search = { path = "crates/web_search" }
web_search_providers = { path = "crates/web_search_providers" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zed_env_vars = { path = "crates/zed_env_vars" }
edit_prediction = { path = "crates/edit_prediction" }
zeta_prompt = { path = "crates/zeta_prompt" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
ztracing = { path = "crates/ztracing" }
ztracing_macro = { path = "crates/ztracing_macro" }
#
# External crates
#
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
ashpd = { version = "0.11", default-features = false, features = ["async-std"] }
async-compat = "0.2.1"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = "0.1"
async-fs = "2.1"
async-lock = "2.1"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.5.1"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.31.0"
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
aws-config = { version = "1.8.10", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.8", features = [
"hardcoded-credentials",
] }
aws-sdk-bedrockruntime = { version = "1.112.0", features = [
"behavior-version-latest",
] }
aws-smithy-runtime-api = { version = "1.9.2", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.4", features = ["http-body-1-x"] }
backtrace = "0.3"
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
brotli = "8.0.2"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
cfg-if = "1.0.3"
chardetng = "0.1"
chrono = { version = "0.4", features = ["serde"] }
ciborium = "0.2"
circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive", "wrap_help"] }
cocoa = "=0.26.0"
cocoa-foundation = "=0.2.0"
convert_case = "0.8.0"
core-foundation = "=0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
cpal = "0.16"
crash-handler = "0.6"
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
documented = "0.9.1"
dotenvy = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
encoding_rs = "0.8"
exec = "0.3.1"
fancy-regex = "0.16.0"
fork = "0.4.0"
futures = "0.3"
futures-lite = "1.13"
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "09acfdf2bd5c1d6254abefd609c808ff73547b2c" }
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
human_bytes = "0.4.1"
html5ever = "0.27.0"
http = "1.1"
http-body = "1.0"
hyper = "0.14"
ignore = "0.4.22"
image = "0.25.1"
imara-diff = "0.1.8"
indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
json_dotpath = "1.1"
jsonschema = "0.37.0"
jsonwebtoken = "9.3"
jupyter-protocol = "0.10.0"
jupyter-websocket-client = "0.15.0"
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "b71ab4eeb27d9758be8092020a46fe33fbca4e33" }
mach2 = "0.5"
markup5ever_rcdom = "0.3.0"
metal = "0.29"
minidumper = "0.8"
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = "0.15.0"
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
objc2-foundation = { version = "=0.3.1", default-features = false, features = [
"NSArray",
"NSAttributedString",
"NSBundle",
"NSCoder",
"NSData",
"NSDate",
"NSDictionary",
"NSEnumerator",
"NSError",
"NSGeometry",
"NSNotification",
"NSNull",
"NSObjCRuntime",
"NSObject",
"NSProcessInfo",
"NSRange",
"NSRunLoop",
"NSString",
"NSURL",
"NSUndoManager",
"NSValue",
"objc2-core-foundation",
"std"
] }
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
pciid-parser = "0.8.0"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
portable-pty = "0.9.0"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
proc-macro2 = "1.0.93"
profiling = "1"
prost = "0.9"
prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.9"
rayon = "1.8"
regex = "1.5"
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
"charset",
"http2",
"macos-system-configuration",
"multipart",
"rustls-tls-native-roots",
"socks",
"stream",
], package = "zed-reqwest", version = "0.12.15-zed" }
rsa = "0.9.6"
runtimelib = { version = "0.30.0", default-features = false, features = [
"async-dispatcher-runtime", "aws-lc-rs"
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = { version = "1.0", features = ["serde"] }
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
simplelog = "0.12.2"
slotmap = "1.0.6"
smallvec = { version = "1.6", features = ["union", "const_new"] }
smol = "2.0"
sqlformat = "0.2"
stacksafe = "0.1"
streaming-iterator = "0.1"
strsim = "0.11"
strum = { version = "0.27.2", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
sys-locale = "0.3.1"
sysinfo = "0.37.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "2570c4387a8505fb8f1d3f3557454b474f1e8271" }
time = { version = "0.3", features = [
"macros",
"parsing",
"serde",
"serde-well-known",
"formatting",
"local-offset",
] }
tiny_http = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io", "tokio"] }
toml = "0.8"
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.26", features = ["wasm"] }
tree-sitter-bash = "0.25.1"
tree-sitter-c = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "2e886870578eeba1927a2dc4bd2e2b3f598c5f9a", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
tree-sitter-html = "0.23"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-python = "0.25"
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
tree-sitter-rust = "0.24"
tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
tracing = "0.1.40"
unicase = "2.6"
unicode-script = "0.5.7"
unicode-segmentation = "1.10"
unindent = "0.2.0"
url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.5"
wasm-encoder = "0.221"
wasmparser = "0.221"
wasmtime = { version = "33", default-features = false, features = [
"async",
"demangle",
"runtime",
"cranelift",
"component-model",
"incremental-cache",
"parallel-compilation",
] }
wasmtime-wasi = "33"
wax = "0.6"
which = "6.0.0"
windows-core = "0.61"
yawc = "0.2.5"
zeroize = "1.8"
zstd = "0.11"
[workspace.dependencies.windows]
version = "0.61"
@@ -770,12 +58,6 @@ features = [
"Win32_UI_WindowsAndMessaging",
]
[patch.crates-io]
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
calloop = { git = "https://github.com/zed-industries/calloop" }
[profile.dev]
split-debuginfo = "unpacked"
# https://github.com/rust-lang/cargo/issues/16104
@@ -897,13 +179,157 @@ large_enum_variant = "allow"
# Boolean expressions can be hard to read, requiring only the minimal form gets in the way
nonminimal_bool = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",
"cbindgen",
"prost_build",
[workspace.dependencies]
anyhow = "1.0.86"
ashpd = { version = "0.11", default-features = false, features = ["async-std"] }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-fs = "2.1"
async-tar = "0.5.1"
async-task = "4.7"
async-trait = "0.1"
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
backtrace = "0.3"
bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
bytes = "1.0"
chrono = { version = "0.4", features = ["serde"] }
circular-buffer = "1.0"
clock = { path = "crates/clock" }
cocoa = "=0.26.0"
cocoa-foundation = "=0.2.0"
collections = { path = "crates/collections", version = "0.1.0" }
convert_case = "0.8.0"
core-foundation = "=0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
derive_more = "0.99.17"
derive_refineable = { path = "crates/refineable/derive_refineable" }
dirs = "4.0"
ec4rs = "1.1"
env_logger = "0.11"
fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
futures = "0.3"
futures-lite = "1.13"
git = { path = "crates/git" }
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
heck = "0.5"
http = "1.1"
http-body = "1.0"
http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
libc = "0.2"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
mach2 = "0.5"
media = { path = "crates/media" }
metal = "0.29"
migrator = { path = "crates/migrator" }
naga = { version = "25.0", features = ["wgsl-in"] }
net = { path = "crates/net" }
nix = "0.29"
objc = "0.2"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
paths = { path = "crates/paths" }
perf = { path = "tooling/perf" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
proc-macro2 = "1.0.93"
profiling = "1"
prost = "0.9"
prost-build = "0.9"
proto = { path = "crates/proto" }
quote = "1.0.9"
rand = "0.9"
rayon = "1.8"
refineable = { path = "crates/refineable" }
regex = "1.5"
release_channel = { path = "crates/release_channel" }
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
"charset",
"http2",
"macos-system-configuration",
"multipart",
"rustls-tls-native-roots",
"socks",
"stream",
], package = "zed-reqwest", version = "0.12.15-zed" }
reqwest_client = { path = "crates/reqwest_client" }
rope = { path = "crates/rope" }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = { version = "1.0", features = ["serde"] }
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
settings = { path = "crates/settings" }
settings_json = { path = "crates/settings_json" }
settings_macros = { path = "crates/settings_macros" }
sha2 = "0.10"
shlex = "1.3.0"
slotmap = "1.0.6"
smallvec = { version = "1.6", features = ["union", "const_new"] }
smol = "2.0"
stacksafe = "0.1"
streaming-iterator = "0.1"
strum = { version = "0.27.2", features = ["derive"] }
sum_tree = { path = "crates/sum_tree" }
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
take-until = "0.2.0"
tempfile = "3.20.0"
text = { path = "crates/text" }
theme = { path = "crates/theme" }
thiserror = "2.0.12"
time = { version = "0.3", features = [
"macros",
"parsing",
"serde",
"component",
"documented",
"sea-orm-macros",
]
"serde-well-known",
"formatting",
"local-offset",
] }
tokio = { version = "1" }
tracing = "0.1.40"
tree-sitter = { version = "0.26", features = ["wasm"] }
tree-sitter-json = "0.24"
unicase = "2.6"
unicode-segmentation = "1.10"
unindent = "0.2.0"
url = "2.2"
urlencoding = "2.1.2"
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.5"
which = "6.0.0"
windows-core = "0.61"
zeroize = "1.8"
zlog = { path = "crates/zlog" }
ztracing = { path = "crates/ztracing" }
ztracing_macro = { path = "crates/ztracing_macro" }

909
Cargo.toml.full Normal file
View File

@@ -0,0 +1,909 @@
[workspace]
resolver = "2"
members = [
"crates/acp_tools",
"crates/acp_thread",
"crates/action_log",
"crates/activity_indicator",
"crates/agent",
"crates/agent_servers",
"crates/agent_settings",
"crates/agent_ui",
"crates/agent_ui_v2",
"crates/ai_onboarding",
"crates/anthropic",
"crates/askpass",
"crates/assets",
"crates/assistant_text_thread",
"crates/assistant_slash_command",
"crates/assistant_slash_commands",
"crates/audio",
"crates/auto_update",
"crates/auto_update_helper",
"crates/auto_update_ui",
"crates/aws_http_client",
"crates/bedrock",
"crates/breadcrumbs",
"crates/buffer_diff",
"crates/call",
"crates/channel",
"crates/cli",
"crates/client",
"crates/clock",
"crates/cloud_api_client",
"crates/cloud_api_types",
"crates/cloud_llm_client",
"crates/collab",
"crates/collab_ui",
"crates/collections",
"crates/command_palette",
"crates/command_palette_hooks",
"crates/component",
"crates/context_server",
"crates/copilot",
"crates/crashes",
"crates/credentials_provider",
"crates/dap",
"crates/dap_adapters",
"crates/db",
"crates/debug_adapter_extension",
"crates/debugger_tools",
"crates/debugger_ui",
"crates/deepseek",
"crates/denoise",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_types",
"crates/edit_prediction_ui",
"crates/edit_prediction_context",
"crates/editor",
"crates/eval",
"crates/eval_utils",
"crates/explorer_command_injector",
"crates/extension",
"crates/extension_api",
"crates/extension_cli",
"crates/extension_host",
"crates/extensions_ui",
"crates/feature_flags",
"crates/feedback",
"crates/file_finder",
"crates/file_icons",
"crates/fs",
"crates/fs_benchmarks",
"crates/fsevent",
"crates/fuzzy",
"crates/git",
"crates/git_hosting_providers",
"crates/git_ui",
"crates/go_to_line",
"crates/google_ai",
"crates/gpui",
"crates/gpui_macros",
"crates/gpui_tokio",
"crates/html_to_markdown",
"crates/http_client",
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
"crates/inspector_ui",
"crates/install_cli",
"crates/journal",
"crates/json_schema_store",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_models",
"crates/language_onboarding",
"crates/language_selector",
"crates/language_tools",
"crates/languages",
"crates/line_ending_selector",
"crates/livekit_api",
"crates/livekit_client",
"crates/lmstudio",
"crates/lsp",
"crates/markdown",
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/migrator",
"crates/mistral",
"crates/miniprofiler_ui",
"crates/multi_buffer",
"crates/nc",
"crates/net",
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
"crates/onboarding",
"crates/open_ai",
"crates/open_router",
"crates/outline",
"crates/outline_panel",
"crates/panel",
"crates/paths",
"crates/picker",
"crates/prettier",
"crates/project",
"crates/project_benchmarks",
"crates/project_panel",
"crates/project_symbols",
"crates/prompt_store",
"crates/proto",
"crates/recent_projects",
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/scheduler",
"crates/remote",
"crates/remote_server",
"crates/repl",
"crates/reqwest_client",
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rules_library",
"crates/schema_generator",
"crates/search",
"crates/session",
"crates/settings",
"crates/settings_json",
"crates/settings_macros",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
"crates/sqlez",
"crates/sqlez_macros",
"crates/story",
"crates/storybook",
"crates/streaming_diff",
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/codestral",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
"crates/telemetry",
"crates/telemetry_events",
"crates/terminal",
"crates/terminal_view",
"crates/text",
"crates/theme",
"crates/theme_extension",
"crates/theme_importer",
"crates/theme_selector",
"crates/time_format",
"crates/title_bar",
"crates/toolchain_selector",
"crates/ui",
"crates/ui_input",
"crates/ui_macros",
"crates/ui_prompt",
"crates/util",
"crates/util_macros",
"crates/vercel",
"crates/vim",
"crates/vim_mode_setting",
"crates/which_key",
"crates/watch",
"crates/web_search",
"crates/web_search_providers",
"crates/workspace",
"crates/worktree",
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
"crates/zed_env_vars",
"crates/edit_prediction_cli",
"crates/zeta_prompt",
"crates/zlog",
"crates/zlog_settings",
"crates/ztracing",
"crates/ztracing_macro",
#
# Extensions
#
"extensions/glsl",
"extensions/html",
"extensions/proto",
"extensions/slash-commands-example",
"extensions/test-extension",
#
# Tooling
#
"tooling/perf",
"tooling/xtask",
]
default-members = ["crates/zed"]
[workspace.package]
publish = false
edition = "2024"
[workspace.dependencies]
#
# Workspace member crates
#
acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" }
activity_indicator = { path = "crates/activity_indicator" }
agent_ui = { path = "crates/agent_ui" }
agent_ui_v2 = { path = "crates/agent_ui_v2" }
agent_settings = { path = "crates/agent_settings" }
agent_servers = { path = "crates/agent_servers" }
ai_onboarding = { path = "crates/ai_onboarding" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
assistant_text_thread = { path = "crates/assistant_text_thread" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
auto_update_ui = { path = "crates/auto_update_ui" }
aws_http_client = { path = "crates/aws_http_client" }
bedrock = { path = "crates/bedrock" }
breadcrumbs = { path = "crates/breadcrumbs" }
buffer_diff = { path = "crates/buffer_diff" }
call = { path = "crates/call" }
channel = { path = "crates/channel" }
cli = { path = "crates/cli" }
client = { path = "crates/client" }
clock = { path = "crates/clock" }
cloud_api_client = { path = "crates/cloud_api_client" }
cloud_api_types = { path = "crates/cloud_api_types" }
cloud_llm_client = { path = "crates/cloud_llm_client" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections", version = "0.1.0" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
crashes = { path = "crates/crashes" }
credentials_provider = { path = "crates/credentials_provider" }
crossbeam = "0.8.4"
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
debug_adapter_extension = { path = "crates/debug_adapter_extension" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }
derive_refineable = { path = "crates/refineable/derive_refineable" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
eval_utils = { path = "crates/eval_utils" }
extension = { path = "crates/extension" }
extension_host = { path = "crates/extension_host" }
extensions_ui = { path = "crates/extensions_ui" }
feature_flags = { path = "crates/feature_flags" }
feedback = { path = "crates/feedback" }
file_finder = { path = "crates/file_finder" }
file_icons = { path = "crates/file_icons" }
fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
git = { path = "crates/git" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
edit_prediction_types = { path = "crates/edit_prediction_types" }
edit_prediction_ui = { path = "crates/edit_prediction_ui" }
edit_prediction_context = { path = "crates/edit_prediction_context" }
inspector_ui = { path = "crates/inspector_ui" }
install_cli = { path = "crates/install_cli" }
journal = { path = "crates/journal" }
json_schema_store = { path = "crates/json_schema_store" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
language_onboarding = { path = "crates/language_onboarding" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
line_ending_selector = { path = "crates/line_ending_selector" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
svg_preview = { path = "crates/svg_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" }
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
nc = { path = "crates/nc" }
net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
onboarding = { path = "crates/onboarding" }
open_ai = { path = "crates/open_ai" }
open_router = { path = "crates/open_router", features = ["schemars"] }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
panel = { path = "crates/panel" }
paths = { path = "crates/paths" }
perf = { path = "tooling/perf" }
picker = { path = "crates/picker" }
prettier = { path = "crates/prettier" }
settings_profile_selector = { path = "crates/settings_profile_selector" }
project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
prompt_store = { path = "crates/prompt_store" }
proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
remote = { path = "crates/remote" }
remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rodio = { git = "https://github.com/RustAudio/rodio", rev ="e2074c6c2acf07b57cf717e076bdda7a9ac6e70b", features = ["wav", "playback", "wav_output", "recording"] }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_json = { path = "crates/settings_json" }
settings_macros = { path = "crates/settings_macros" }
settings_ui = { path = "crates/settings_ui" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
snippets_ui = { path = "crates/snippets_ui" }
sqlez = { path = "crates/sqlez" }
sqlez_macros = { path = "crates/sqlez_macros" }
story = { path = "crates/story" }
streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
codestral = { path = "crates/codestral" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
telemetry = { path = "crates/telemetry" }
telemetry_events = { path = "crates/telemetry_events" }
terminal = { path = "crates/terminal" }
terminal_view = { path = "crates/terminal_view" }
text = { path = "crates/text" }
theme = { path = "crates/theme" }
theme_extension = { path = "crates/theme_extension" }
theme_selector = { path = "crates/theme_selector" }
time_format = { path = "crates/time_format" }
title_bar = { path = "crates/title_bar" }
toolchain_selector = { path = "crates/toolchain_selector" }
ui = { path = "crates/ui" }
ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
ui_prompt = { path = "crates/ui_prompt" }
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
vercel = { path = "crates/vercel" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
which_key = { path = "crates/which_key" }
watch = { path = "crates/watch" }
web_search = { path = "crates/web_search" }
web_search_providers = { path = "crates/web_search_providers" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zed_env_vars = { path = "crates/zed_env_vars" }
edit_prediction = { path = "crates/edit_prediction" }
zeta_prompt = { path = "crates/zeta_prompt" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
ztracing = { path = "crates/ztracing" }
ztracing_macro = { path = "crates/ztracing_macro" }
#
# External crates
#
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
ashpd = { version = "0.11", default-features = false, features = ["async-std"] }
async-compat = "0.2.1"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = "0.1"
async-fs = "2.1"
async-lock = "2.1"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.5.1"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.31.0"
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
aws-config = { version = "1.8.10", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.8", features = [
"hardcoded-credentials",
] }
aws-sdk-bedrockruntime = { version = "1.112.0", features = [
"behavior-version-latest",
] }
aws-smithy-runtime-api = { version = "1.9.2", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.4", features = ["http-body-1-x"] }
backtrace = "0.3"
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
brotli = "8.0.2"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
cfg-if = "1.0.3"
chardetng = "0.1"
chrono = { version = "0.4", features = ["serde"] }
ciborium = "0.2"
circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive", "wrap_help"] }
cocoa = "=0.26.0"
cocoa-foundation = "=0.2.0"
convert_case = "0.8.0"
core-foundation = "=0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
cpal = "0.16"
crash-handler = "0.6"
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
documented = "0.9.1"
dotenvy = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
encoding_rs = "0.8"
exec = "0.3.1"
fancy-regex = "0.16.0"
fork = "0.4.0"
futures = "0.3"
futures-lite = "1.13"
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "09acfdf2bd5c1d6254abefd609c808ff73547b2c" }
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
human_bytes = "0.4.1"
html5ever = "0.27.0"
http = "1.1"
http-body = "1.0"
hyper = "0.14"
ignore = "0.4.22"
image = "0.25.1"
imara-diff = "0.1.8"
indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
json_dotpath = "1.1"
jsonschema = "0.37.0"
jsonwebtoken = "9.3"
jupyter-protocol = "0.10.0"
jupyter-websocket-client = "0.15.0"
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "b71ab4eeb27d9758be8092020a46fe33fbca4e33" }
mach2 = "0.5"
markup5ever_rcdom = "0.3.0"
metal = "0.29"
minidumper = "0.8"
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = "0.15.0"
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
objc2-foundation = { version = "=0.3.1", default-features = false, features = [
"NSArray",
"NSAttributedString",
"NSBundle",
"NSCoder",
"NSData",
"NSDate",
"NSDictionary",
"NSEnumerator",
"NSError",
"NSGeometry",
"NSNotification",
"NSNull",
"NSObjCRuntime",
"NSObject",
"NSProcessInfo",
"NSRange",
"NSRunLoop",
"NSString",
"NSURL",
"NSUndoManager",
"NSValue",
"objc2-core-foundation",
"std"
] }
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
pciid-parser = "0.8.0"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
portable-pty = "0.9.0"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
proc-macro2 = "1.0.93"
profiling = "1"
prost = "0.9"
prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.9"
rayon = "1.8"
regex = "1.5"
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
"charset",
"http2",
"macos-system-configuration",
"multipart",
"rustls-tls-native-roots",
"socks",
"stream",
], package = "zed-reqwest", version = "0.12.15-zed" }
rsa = "0.9.6"
runtimelib = { version = "0.30.0", default-features = false, features = [
"async-dispatcher-runtime", "aws-lc-rs"
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = { version = "1.0", features = ["serde"] }
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
simplelog = "0.12.2"
slotmap = "1.0.6"
smallvec = { version = "1.6", features = ["union", "const_new"] }
smol = "2.0"
sqlformat = "0.2"
stacksafe = "0.1"
streaming-iterator = "0.1"
strsim = "0.11"
strum = { version = "0.27.2", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
sys-locale = "0.3.1"
sysinfo = "0.37.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "2570c4387a8505fb8f1d3f3557454b474f1e8271" }
time = { version = "0.3", features = [
"macros",
"parsing",
"serde",
"serde-well-known",
"formatting",
"local-offset",
] }
tiny_http = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io", "tokio"] }
toml = "0.8"
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.26", features = ["wasm"] }
tree-sitter-bash = "0.25.1"
tree-sitter-c = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "2e886870578eeba1927a2dc4bd2e2b3f598c5f9a", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
tree-sitter-html = "0.23"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-python = "0.25"
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
tree-sitter-rust = "0.24"
tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
tracing = "0.1.40"
unicase = "2.6"
unicode-script = "0.5.7"
unicode-segmentation = "1.10"
unindent = "0.2.0"
url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.5"
wasm-encoder = "0.221"
wasmparser = "0.221"
wasmtime = { version = "33", default-features = false, features = [
"async",
"demangle",
"runtime",
"cranelift",
"component-model",
"incremental-cache",
"parallel-compilation",
] }
wasmtime-wasi = "33"
wax = "0.6"
which = "6.0.0"
windows-core = "0.61"
yawc = "0.2.5"
zeroize = "1.8"
zstd = "0.11"
[workspace.dependencies.windows]
version = "0.61"
features = [
"Foundation_Numerics",
"Storage_Search",
"Storage_Streams",
"System_Threading",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D_Fxc",
"Win32_Graphics_DirectComposition",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dwm",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
"Win32_Security_Cryptography",
"Win32_Storage_FileSystem",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_Console",
"Win32_System_DataExchange",
"Win32_System_IO",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_Performance",
"Win32_System_Pipes",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Variant",
"Win32_System_WinRT",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
"Win32_UI_Input_Ime",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem",
"Win32_UI_WindowsAndMessaging",
]
[patch.crates-io]
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
calloop = { git = "https://github.com/zed-industries/calloop" }
[profile.dev]
split-debuginfo = "unpacked"
# https://github.com/rust-lang/cargo/issues/16104
incremental = false
codegen-units = 16
# mirror configuration for crates compiled for the build platform
# (without this cargo will compile ~400 crates twice)
[profile.dev.build-override]
codegen-units = 16
[profile.dev.package]
# proc-macros start
gpui_macros = { opt-level = 3 }
derive_refineable = { opt-level = 3 }
settings_macros = { opt-level = 3 }
sqlez_macros = { opt-level = 3, codegen-units = 1 }
ui_macros = { opt-level = 3 }
util_macros = { opt-level = 3 }
quote = { opt-level = 3 }
syn = { opt-level = 3 }
proc-macro2 = { opt-level = 3 }
# proc-macros end
taffy = { opt-level = 3 }
resvg = { opt-level = 3 }
wasmtime = { opt-level = 3 }
# Build single-source-file crates with cg=1 as it helps make `cargo build` of a whole workspace a bit faster
activity_indicator = { codegen-units = 1 }
assets = { codegen-units = 1 }
breadcrumbs = { codegen-units = 1 }
collections = { codegen-units = 1 }
command_palette = { codegen-units = 1 }
command_palette_hooks = { codegen-units = 1 }
feature_flags = { codegen-units = 1 }
file_icons = { codegen-units = 1 }
fsevent = { codegen-units = 1 }
image_viewer = { codegen-units = 1 }
edit_prediction_ui = { codegen-units = 1 }
install_cli = { codegen-units = 1 }
journal = { codegen-units = 1 }
json_schema_store = { codegen-units = 1 }
lmstudio = { codegen-units = 1 }
menu = { codegen-units = 1 }
notifications = { codegen-units = 1 }
ollama = { codegen-units = 1 }
outline = { codegen-units = 1 }
paths = { codegen-units = 1 }
prettier = { codegen-units = 1 }
project_symbols = { codegen-units = 1 }
refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 }
session = { codegen-units = 1 }
snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 }
story = { codegen-units = 1 }
supermaven_api = { codegen-units = 1 }
telemetry_events = { codegen-units = 1 }
theme_selector = { codegen-units = 1 }
time_format = { codegen-units = 1 }
ui_input = { codegen-units = 1 }
zed_actions = { codegen-units = 1 }
[profile.release]
debug = "limited"
lto = "thin"
codegen-units = 1
[profile.release.package]
zed = { codegen-units = 16 }
[profile.release-fast]
inherits = "release"
debug = "full"
lto = false
codegen-units = 16
[workspace.lints.rust]
unexpected_cfgs = { level = "allow" }
[workspace.lints.clippy]
dbg_macro = "deny"
todo = "deny"
declare_interior_mutable_const = "deny"
redundant_clone = "deny"
disallowed_methods = "deny"
# We currently do not restrict any style rules
# as it slows down shipping code to Zed.
#
# Running ./script/clippy can take several minutes, and so it's
# common to skip that step and let CI do it. Any unexpected failures
# (which also take minutes to discover) thus require switching back
# to an old branch, manual fixing, and re-pushing.
#
# In the future we could improve this by either making sure
# Zed can surface clippy errors in diagnostics (in addition to the
# rust-analyzer errors), or by having CI fix style nits automatically.
style = { level = "allow", priority = -1 }
# Individual rules that have violations in the codebase:
type_complexity = "allow"
let_underscore_future = "allow"
# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
# warning on this rule produces a lot of noise.
single_range_in_vec_init = "allow"
# in Rust it can be very tedious to reduce argument count without
# running afoul of the borrow checker.
too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
# Boolean expressions can be hard to read, requiring only the minimal form gets in the way
nonminimal_bool = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",
"cbindgen",
"prost_build",
"serde",
"component",
"documented",
"sea-orm-macros",
]

View File

@@ -227,6 +227,7 @@
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -293,6 +294,7 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -304,6 +306,7 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{

View File

@@ -267,6 +267,7 @@
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-k l": "agent::OpenRulesLibrary",
"alt-tab": "agent::CycleFavoriteModels",
"cmd-shift-v": "agent::PasteRaw",
},
},
{
@@ -335,6 +336,7 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"cmd-shift-v": "agent::PasteRaw",
},
},
{
@@ -347,6 +349,7 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"cmd-shift-v": "agent::PasteRaw",
},
},
{

View File

@@ -227,6 +227,7 @@
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -296,6 +297,7 @@
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -308,6 +310,7 @@
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{

View File

@@ -192,6 +192,7 @@ pub struct ToolCall {
pub locations: Vec<acp::ToolCallLocation>,
pub resolved_locations: Vec<Option<AgentLocation>>,
pub raw_input: Option<serde_json::Value>,
pub raw_input_markdown: Option<Entity<Markdown>>,
pub raw_output: Option<serde_json::Value>,
}
@@ -222,6 +223,11 @@ impl ToolCall {
}
}
let raw_input_markdown = tool_call
.raw_input
.as_ref()
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
let result = Self {
id: tool_call.tool_call_id,
label: cx
@@ -232,6 +238,7 @@ impl ToolCall {
resolved_locations: Vec::default(),
status,
raw_input: tool_call.raw_input,
raw_input_markdown,
raw_output: tool_call.raw_output,
};
Ok(result)
@@ -307,6 +314,7 @@ impl ToolCall {
}
if let Some(raw_input) = raw_input {
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
self.raw_input = Some(raw_input);
}
@@ -1355,6 +1363,7 @@ impl AcpThread {
locations: Vec::new(),
resolved_locations: Vec::new(),
raw_input: None,
raw_input_markdown: None,
raw_output: None,
};
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);

View File

@@ -34,7 +34,7 @@ use theme::ThemeSettings;
use ui::prelude::*;
use util::{ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::Chat;
use zed_actions::agent::{Chat, PasteRaw};
pub struct MessageEditor {
mention_set: Entity<MentionSet>,
@@ -543,6 +543,9 @@ impl MessageEditor {
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let editor_clipboard_selections = cx
.read_from_clipboard()
.and_then(|item| item.entries().first().cloned())
@@ -553,133 +556,127 @@ impl MessageEditor {
_ => None,
});
let has_file_context = editor_clipboard_selections
.as_ref()
.is_some_and(|selections| {
selections
.iter()
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
});
if has_file_context {
if let Some((workspace, selections)) =
self.workspace.upgrade().zip(editor_clipboard_selections)
{
let Some(first_selection) = selections.first() else {
return;
};
if let Some(file_path) = &first_selection.file_path {
// In case someone pastes selections from another window
// with a different project, we don't want to insert the
// crease (containing the absolute path) since the agent
// cannot access files outside the project.
let is_in_project = workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some();
if !is_in_project {
return;
}
}
cx.stop_propagation();
let insertion_target = self
.editor
.read(cx)
.selections
.newest_anchor()
.start
.text_anchor;
let project = workspace.read(cx).project().clone();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let crease_text =
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
let mention_uri = MentionUri::Selection {
abs_path: Some(file_path.clone()),
line_range: line_range.clone(),
};
let mention_text = mention_uri.as_link().to_string();
let (excerpt_id, text_anchor, content_len) =
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx);
let snapshot = buffer.snapshot(cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.as_singleton().unwrap();
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
editor.insert(&mention_text, window, cx);
editor.insert(" ", window, cx);
(*excerpt_id, text_anchor, mention_text.len())
});
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
crease_text.into(),
mention_uri.icon_path(cx),
None,
self.editor.clone(),
window,
cx,
) else {
continue;
};
drop(tx);
let mention_task = cx
.spawn({
let project = project.clone();
async move |_, cx| {
let project_path = project
.update(cx, |project, cx| {
project.project_path_for_absolute_path(&file_path, cx)
})
.map_err(|e| e.to_string())?
.ok_or_else(|| "project path not found".to_string())?;
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path, cx)
})
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
buffer
.update(cx, |buffer, cx| {
let start = Point::new(*line_range.start(), 0)
.min(buffer.max_point());
let end = Point::new(*line_range.end() + 1, 0)
.min(buffer.max_point());
let content =
buffer.text_for_range(start..end).collect();
Mention::Text {
content,
tracked_buffers: vec![cx.entity()],
}
})
.map_err(|e| e.to_string())
}
})
.shared();
self.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
});
}
}
return;
// Insert creases for pasted clipboard selections that:
// 1. Contain exactly one selection
// 2. Have an associated file path
// 3. Span multiple lines (not single-line selections)
// 4. Belong to a file that exists in the current project
let should_insert_creases = util::maybe!({
let selections = editor_clipboard_selections.as_ref()?;
if selections.len() > 1 {
return Some(false);
}
let selection = selections.first()?;
let file_path = selection.file_path.as_ref()?;
let line_range = selection.line_range.as_ref()?;
if line_range.start() == line_range.end() {
return Some(false);
}
Some(
workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some(),
)
})
.unwrap_or(false);
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
let insertion_target = self
.editor
.read(cx)
.selections
.newest_anchor()
.start
.text_anchor;
let project = workspace.read(cx).project().clone();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let crease_text =
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
let mention_uri = MentionUri::Selection {
abs_path: Some(file_path.clone()),
line_range: line_range.clone(),
};
let mention_text = mention_uri.as_link().to_string();
let (excerpt_id, text_anchor, content_len) =
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx);
let snapshot = buffer.snapshot(cx);
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
editor.insert(&mention_text, window, cx);
editor.insert(" ", window, cx);
(*excerpt_id, text_anchor, mention_text.len())
});
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
crease_text.into(),
mention_uri.icon_path(cx),
None,
self.editor.clone(),
window,
cx,
) else {
continue;
};
drop(tx);
let mention_task = cx
.spawn({
let project = project.clone();
async move |_, cx| {
let project_path = project
.update(cx, |project, cx| {
project.project_path_for_absolute_path(&file_path, cx)
})
.map_err(|e| e.to_string())?
.ok_or_else(|| "project path not found".to_string())?;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
buffer
.update(cx, |buffer, cx| {
let start = Point::new(*line_range.start(), 0)
.min(buffer.max_point());
let end = Point::new(*line_range.end() + 1, 0)
.min(buffer.max_point());
let content = buffer.text_for_range(start..end).collect();
Mention::Text {
content,
tracked_buffers: vec![cx.entity()],
}
})
.map_err(|e| e.to_string())
}
})
.shared();
self.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
});
}
}
return;
}
if self.prompt_capabilities.borrow().image
@@ -690,6 +687,13 @@ impl MessageEditor {
}
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.clone();
window.defer(cx, move |window, cx| {
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
});
}
pub fn insert_dragged_files(
&mut self,
paths: Vec<project::ProjectPath>,
@@ -967,6 +971,7 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::chat_with_follow))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::paste_raw))
.capture_action(cx.listener(Self::paste))
.flex_1()
.child({

View File

@@ -221,7 +221,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let favorites = if self.selector.supports_favorites() {
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
AgentSettings::get_global(cx).favorite_model_ids()
} else {
Default::default()
};
@@ -242,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
info_list_to_picker_entries(filtered_models, favorites);
info_list_to_picker_entries(filtered_models, &favorites);
// Finds the currently selected model in the list
let new_index = this
.delegate
@@ -406,7 +406,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn info_list_to_picker_entries(
model_list: AgentModelList,
favorites: Arc<HashSet<ModelId>>,
favorites: &HashSet<ModelId>,
) -> Vec<AcpModelPickerEntry> {
let mut entries = Vec::new();
@@ -572,13 +572,11 @@ mod tests {
}
}
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
Arc::new(
models
.into_iter()
.map(|m| ModelId::new(m.to_string()))
.collect(),
)
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
models
.into_iter()
.map(|m| ModelId::new(m.to_string()))
.collect()
}
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
@@ -609,7 +607,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, favorites);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),
@@ -625,7 +623,7 @@ mod tests {
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
let favorites = create_favorites(vec![]);
let entries = info_list_to_picker_entries(models, favorites);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),
@@ -641,7 +639,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, favorites);
let entries = info_list_to_picker_entries(models, &favorites);
for entry in &entries {
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
@@ -662,7 +660,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
let entries = info_list_to_picker_entries(models, favorites);
let entries = info_list_to_picker_entries(models, &favorites);
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
@@ -683,7 +681,7 @@ mod tests {
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, favorites);
let entries = info_list_to_picker_entries(models, &favorites);
let labels = get_entry_labels(&entries);
assert_eq!(
@@ -723,7 +721,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, favorites);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),

View File

@@ -34,7 +34,7 @@ use language::Buffer;
use language_model::LanguageModelRegistry;
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use project::{Project, ProjectEntryId};
use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
@@ -260,6 +260,7 @@ impl ThreadFeedbackState {
pub struct AcpThreadView {
agent: Rc<dyn AgentServer>,
agent_server_store: Entity<AgentServerStore>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_state: ThreadState,
@@ -406,6 +407,7 @@ impl AcpThreadView {
Self {
agent: agent.clone(),
agent_server_store,
workspace: workspace.clone(),
project: project.clone(),
entry_view_state,
@@ -737,7 +739,7 @@ impl AcpThreadView {
cx: &mut App,
) {
let agent_name = agent.name();
let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
let registry = LanguageModelRegistry::global(cx);
let sub = window.subscribe(&registry, cx, {
@@ -779,7 +781,6 @@ impl AcpThreadView {
configuration_view,
description: err
.description
.clone()
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
_subscription: subscription,
};
@@ -1088,10 +1089,7 @@ impl AcpThreadView {
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired {
description: None,
provider_id: None,
},
AuthRequired::new(),
agent,
connection,
window,
@@ -1663,44 +1661,6 @@ impl AcpThreadView {
});
return;
}
} else if method.0.as_ref() == "anthropic-api-key" {
let registry = LanguageModelRegistry::global(cx);
let provider = registry
.read(cx)
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
.unwrap();
let this = cx.weak_entity();
let agent = self.agent.clone();
let connection = connection.clone();
window.defer(cx, move |window, cx| {
if !provider.is_authenticated(cx) {
Self::handle_auth_required(
this,
AuthRequired {
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
},
agent,
connection,
window,
cx,
);
} else {
this.update(cx, |this, cx| {
this.thread_state = Self::initial_state(
agent,
None,
this.workspace.clone(),
this.project.clone(),
true,
window,
cx,
)
})
.ok();
}
});
return;
} else if method.0.as_ref() == "vertex-ai"
&& std::env::var("GOOGLE_API_KEY").is_err()
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -2153,6 +2113,7 @@ impl AcpThreadView {
chunks,
indented: _,
}) => {
let mut is_blank = true;
let is_last = entry_ix + 1 == total_entries;
let style = default_markdown_style(false, false, window, cx);
@@ -2162,36 +2123,55 @@ impl AcpThreadView {
.children(chunks.iter().enumerate().filter_map(
|(chunk_ix, chunk)| match chunk {
AssistantMessageChunk::Message { block } => {
block.markdown().map(|md| {
self.render_markdown(md.clone(), style.clone())
.into_any_element()
block.markdown().and_then(|md| {
let this_is_blank = md.read(cx).source().trim().is_empty();
is_blank = is_blank && this_is_blank;
if this_is_blank {
return None;
}
Some(
self.render_markdown(md.clone(), style.clone())
.into_any_element(),
)
})
}
AssistantMessageChunk::Thought { block } => {
block.markdown().map(|md| {
self.render_thinking_block(
entry_ix,
chunk_ix,
md.clone(),
window,
cx,
block.markdown().and_then(|md| {
let this_is_blank = md.read(cx).source().trim().is_empty();
is_blank = is_blank && this_is_blank;
if this_is_blank {
return None;
}
Some(
self.render_thinking_block(
entry_ix,
chunk_ix,
md.clone(),
window,
cx,
)
.into_any_element(),
)
.into_any_element()
})
}
},
))
.into_any();
v_flex()
.px_5()
.py_1p5()
.when(is_first_indented, |this| this.pt_0p5())
.when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(message_body)
.into_any()
if is_blank {
Empty.into_any()
} else {
v_flex()
.px_5()
.py_1p5()
.when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(message_body)
.into_any()
}
}
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
@@ -2223,7 +2203,7 @@ impl AcpThreadView {
div()
.relative()
.w_full()
.pl(rems_from_px(20.0))
.pl_5()
.bg(cx.theme().colors().panel_background.opacity(0.2))
.child(
div()
@@ -2440,6 +2420,12 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let input_output_header = |label: SharedString| {
Label::new(label)
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx)
};
let tool_output_display =
if is_open {
@@ -2481,7 +2467,25 @@ impl AcpThreadView {
| ToolCallStatus::Completed
| ToolCallStatus::Failed
| ToolCallStatus::Canceled => v_flex()
.w_full()
.when(!is_edit && !is_terminal_tool, |this| {
this.mt_1p5().w_full().child(
v_flex()
.ml(rems(0.4))
.px_3p5()
.pb_1()
.gap_1()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
.child(input_output_header("Raw Input:".into()))
.children(tool_call.raw_input_markdown.clone().map(|input| {
self.render_markdown(
input,
default_markdown_style(false, false, window, cx),
)
}))
.child(input_output_header("Output:".into())),
)
})
.children(tool_call.content.iter().enumerate().map(
|(content_ix, content)| {
div().child(self.render_tool_call_content(
@@ -2580,7 +2584,7 @@ impl AcpThreadView {
.gap_px()
.when(is_collapsible, |this| {
this.child(
Disclosure::new(("expand", entry_ix), is_open)
Disclosure::new(("expand-output", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.visible_on_hover(&card_header_id)
@@ -2766,20 +2770,20 @@ impl AcpThreadView {
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
v_flex()
.mt_1p5()
.gap_2()
.when(!card_layout, |this| {
this.ml(rems(0.4))
.px_3p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
})
.when(card_layout, |this| {
this.px_2().pb_2().when(context_ix > 0, |this| {
this.border_t_1()
.pt_2()
.map(|this| {
if card_layout {
this.when(context_ix > 0, |this| {
this.pt_2()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
} else {
this.ml(rems(0.4))
.px_3p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
})
}
})
.text_xs()
.text_color(cx.theme().colors().text_muted)
@@ -3500,138 +3504,119 @@ impl AcpThreadView {
pending_auth_method: Option<&acp::AuthMethodId>,
window: &mut Window,
cx: &Context<Self>,
) -> Div {
let show_description =
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
) -> impl IntoElement {
let auth_methods = connection.auth_methods();
v_flex().flex_1().size_full().justify_end().child(
v_flex()
.p_2()
.pr_3()
.w_full()
.gap_1()
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().status().warning.opacity(0.04))
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::Warning)
.color(Color::Warning)
.size(IconSize::Small),
)
.child(Label::new("Authentication Required").size(LabelSize::Small)),
)
.children(description.map(|desc| {
div().text_ui(cx).child(self.render_markdown(
desc.clone(),
default_markdown_style(false, false, window, cx),
))
}))
.children(
configuration_view
.cloned()
.map(|view| div().w_full().child(view)),
)
.when(show_description, |el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}.{}",
self.agent.name(),
if auth_methods.len() > 1 {
" Please choose one of the following options:"
} else {
""
}
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
})
.when_some(pending_auth_method, |el, _| {
el.child(
h_flex()
.py_4()
.w_full()
.justify_center()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(2),
)
.child(Label::new("Authenticating…").size(LabelSize::Small)),
)
})
.when(!auth_methods.is_empty(), |this| {
this.child(
h_flex()
.justify_end()
.flex_wrap()
.gap_1()
.when(!show_description, |this| {
this.border_t_1()
.mt_1()
.pt_2()
.border_color(cx.theme().colors().border.opacity(0.8))
let agent_display_name = self
.agent_server_store
.read(cx)
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
.unwrap_or_else(|| self.agent.name());
let show_fallback_description = auth_methods.len() > 1
&& configuration_view.is_none()
&& description.is_none()
&& pending_auth_method.is_none();
let auth_buttons = || {
h_flex().justify_end().flex_wrap().gap_1().children(
connection
.auth_methods()
.iter()
.enumerate()
.rev()
.map(|(ix, method)| {
let (method_id, name) = if self.project.read(cx).is_via_remote_server()
&& method.id.0.as_ref() == "oauth-personal"
&& method.name == "Log in with Google"
{
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
} else {
(method.id.0.clone(), method.name.clone())
};
let agent_telemetry_id = connection.telemetry_id();
Button::new(method_id.clone(), name)
.label_size(LabelSize::Small)
.map(|this| {
if ix == 0 {
this.style(ButtonStyle::Tinted(TintColor::Accent))
} else {
this.style(ButtonStyle::Outlined)
}
})
.children(connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
let (method_id, name) = if self
.project
.read(cx)
.is_via_remote_server()
&& method.id.0.as_ref() == "oauth-personal"
&& method.name == "Log in with Google"
{
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
} else {
(method.id.0.clone(), method.name.clone())
};
.when_some(method.description.clone(), |this, description| {
this.tooltip(Tooltip::text(description))
})
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = agent_telemetry_id,
method = method_id
);
let agent_telemetry_id = connection.telemetry_id();
this.authenticate(
acp::AuthMethodId::new(method_id.clone()),
window,
cx,
)
})
})
}),
)
};
Button::new(method_id.clone(), name)
.label_size(LabelSize::Small)
.map(|this| {
if ix == 0 {
this.style(ButtonStyle::Tinted(TintColor::Warning))
} else {
this.style(ButtonStyle::Outlined)
}
})
.when_some(
method.description.clone(),
|this, description| {
this.tooltip(Tooltip::text(description))
},
)
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = agent_telemetry_id,
method = method_id
);
if pending_auth_method.is_some() {
return Callout::new()
.icon(IconName::Info)
.title(format!("Authenticating to {}", agent_display_name))
.actions_slot(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(2)
.into_any_element(),
)
.into_any_element();
}
this.authenticate(
acp::AuthMethodId::new(method_id.clone()),
window,
cx,
)
})
})
},
)),
)
}),
)
Callout::new()
.icon(IconName::Info)
.title(format!("Authenticate to {}", agent_display_name))
.when(auth_methods.len() == 1, |this| {
this.actions_slot(auth_buttons())
})
.description_slot(
v_flex()
.text_ui(cx)
.map(|this| {
if show_fallback_description {
this.child(
Label::new("Choose one of the following authentication options:")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.children(
configuration_view
.cloned()
.map(|view| div().w_full().child(view)),
)
.children(description.map(|desc| {
self.render_markdown(
desc.clone(),
default_markdown_style(false, false, window, cx),
)
}))
}
})
.when(auth_methods.len() > 1, |this| {
this.gap_1().child(auth_buttons())
}),
)
.into_any_element()
}
fn render_load_error(
@@ -5880,10 +5865,6 @@ impl AcpThreadView {
};
let connection = thread.read(cx).connection().clone();
let err = AuthRequired {
description: None,
provider_id: None,
};
this.clear_thread_error(cx);
if let Some(message) = this.in_flight_prompt.take() {
this.message_editor.update(cx, |editor, cx| {
@@ -5892,7 +5873,14 @@ impl AcpThreadView {
}
let this = cx.weak_entity();
window.defer(cx, |window, cx| {
Self::handle_auth_required(this, err, agent, connection, window, cx);
Self::handle_auth_required(
this,
AuthRequired::new(),
agent,
connection,
window,
cx,
);
})
}
}))
@@ -5905,14 +5893,10 @@ impl AcpThreadView {
};
let connection = thread.read(cx).connection().clone();
let err = AuthRequired {
description: None,
provider_id: None,
};
self.clear_thread_error(cx);
let this = cx.weak_entity();
window.defer(cx, |window, cx| {
Self::handle_auth_required(this, err, agent, connection, window, cx);
Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
})
}
@@ -6015,16 +5999,19 @@ impl Render for AcpThreadView {
configuration_view,
pending_auth_method,
..
} => self
.render_auth_required_state(
} => v_flex()
.flex_1()
.size_full()
.justify_end()
.child(self.render_auth_required_state(
connection,
description.as_ref(),
configuration_view.as_ref(),
pending_auth_method.as_ref(),
window,
cx,
)
.into_any(),
))
.into_any_element(),
ThreadState::Loading { .. } => v_flex()
.flex_1()
.child(self.render_recent_history(cx))

View File

@@ -71,7 +71,7 @@ use workspace::{
pane,
searchable::{SearchEvent, SearchableItem},
};
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
use crate::CycleFavoriteModels;
@@ -1698,6 +1698,9 @@ impl TextThreadEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let editor_clipboard_selections = cx
.read_from_clipboard()
.and_then(|item| item.entries().first().cloned())
@@ -1708,84 +1711,101 @@ impl TextThreadEditor {
_ => None,
});
let has_file_context = editor_clipboard_selections
.as_ref()
.is_some_and(|selections| {
selections
.iter()
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
});
// Insert creases for pasted clipboard selections that:
// 1. Contain exactly one selection
// 2. Have an associated file path
// 3. Span multiple lines (not single-line selections)
// 4. Belong to a file that exists in the current project
let should_insert_creases = util::maybe!({
let selections = editor_clipboard_selections.as_ref()?;
if selections.len() > 1 {
return Some(false);
}
let selection = selections.first()?;
let file_path = selection.file_path.as_ref()?;
let line_range = selection.line_range.as_ref()?;
if has_file_context {
if let Some(clipboard_item) = cx.read_from_clipboard() {
if let Some(ClipboardEntry::String(clipboard_text)) =
clipboard_item.entries().first()
{
if let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
if line_range.start() == line_range.end() {
return Some(false);
}
let text = clipboard_text.text();
self.editor.update(cx, |editor, cx| {
let mut current_offset = 0;
let weak_editor = cx.entity().downgrade();
Some(
workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some(),
)
})
.unwrap_or(false);
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let selected_text =
&text[current_offset..current_offset + selection.len];
let fence = assistant_slash_commands::codeblock_fence_for_path(
file_path.to_str(),
Some(line_range.clone()),
);
let formatted_text = format!("{fence}{selected_text}\n```");
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
if let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
let insert_point = editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head();
let start_row = MultiBufferRow(insert_point.row);
let text = clipboard_text.text();
self.editor.update(cx, |editor, cx| {
let mut current_offset = 0;
let weak_editor = cx.entity().downgrade();
editor.insert(&formatted_text, window, cx);
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let selected_text =
&text[current_offset..current_offset + selection.len];
let fence = assistant_slash_commands::codeblock_fence_for_path(
file_path.to_str(),
Some(line_range.clone()),
);
let formatted_text = format!("{fence}{selected_text}\n```");
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(insert_point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
let insert_point = editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head();
let start_row = MultiBufferRow(insert_point.row);
editor.insert("\n", window, cx);
editor.insert(&formatted_text, window, cx);
let crease_text = acp_thread::selection_name(
Some(file_path.as_ref()),
&line_range,
);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(insert_point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
let fold_placeholder = quote_selection_fold_placeholder(
crease_text,
weak_editor.clone(),
);
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
editor.insert("\n", window, cx);
current_offset += selection.len;
if !selection.is_entire_line && current_offset < text.len() {
current_offset += 1;
}
let crease_text = acp_thread::selection_name(
Some(file_path.as_ref()),
&line_range,
);
let fold_placeholder = quote_selection_fold_placeholder(
crease_text,
weak_editor.clone(),
);
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
current_offset += selection.len;
if !selection.is_entire_line && current_offset < text.len() {
current_offset += 1;
}
}
});
return;
}
}
});
return;
}
}
}
@@ -1944,6 +1964,12 @@ impl TextThreadEditor {
}
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.paste(&editor::actions::Paste, window, cx);
});
}
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -2627,6 +2653,7 @@ impl Render for TextThreadEditor {
.capture_action(cx.listener(TextThreadEditor::copy))
.capture_action(cx.listener(TextThreadEditor::cut))
.capture_action(cx.listener(TextThreadEditor::paste))
.on_action(cx.listener(TextThreadEditor::paste_raw))
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
.capture_action(cx.listener(TextThreadEditor::confirm_command))
.on_action(cx.listener(TextThreadEditor::assist))

View File

@@ -103,8 +103,9 @@ impl Model {
pub fn max_output_tokens(&self) -> Option<u64> {
match self {
Self::Chat => Some(8_192),
Self::Reasoner => Some(64_000),
// Their API treats this max against the context window, which means we hit the limit a lot
// Using the default value of None in the API instead
Self::Chat | Self::Reasoner => None,
Self::Custom {
max_output_tokens, ..
} => *max_output_tokens,

View File

@@ -6,7 +6,7 @@ use crate::{
use anyhow::{Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{
App, AppContext as _, Entity, SharedString, Task,
App, AppContext as _, Entity, Global, SharedString, Task,
http_client::{self, AsyncBody, Method},
};
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
@@ -300,14 +300,19 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
impl Global for GlobalMercuryApiKey {}
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
MERCURY_API_KEY
.get_or_init(|| {
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
})
.clone()
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
return global.0.clone();
}
let entity =
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
cx.set_global(GlobalMercuryApiKey(entity.clone()));
entity
}
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use futures::AsyncReadExt as _;
use gpui::{
App, AppContext as _, Entity, SharedString, Task,
App, AppContext as _, Entity, Global, SharedString, Task,
http_client::{self, AsyncBody, Method},
};
use language::{Point, ToOffset as _};
@@ -272,14 +272,19 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
SharedString::new_static("https://autocomplete.sweep.dev");
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
struct GlobalSweepApiKey(Entity<ApiKeyState>);
impl Global for GlobalSweepApiKey {}
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
SWEEP_API_KEY
.get_or_init(|| {
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
})
.clone()
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
return global.0.clone();
}
let entity =
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
cx.set_global(GlobalSweepApiKey(entity.clone()));
entity
}
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

View File

@@ -348,6 +348,61 @@ where
);
}
#[gpui::test]
async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.colorize_brackets = Some(true);
});
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
language_registry.add(markdown_lang());
language_registry.add(rust_lang());
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language_registry(language_registry.clone());
buffer.set_language(Some(markdown_lang()), cx);
});
cx.set_state(indoc! {r#"
fn main() {
let v: Vec<Stringˇ> = vec![];
}
"#});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
r#"fn main«1()1» «1{
let v: Vec<String> = vec!«2[]2»;
}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
2 hsla(29.00, 54.00%, 65.88%, 1.00)
"#,
&bracket_colors_markup(&mut cx),
"Markdown does not colorize <> brackets"
);
cx.update_buffer(|buffer, cx| {
buffer.set_language(Some(rust_lang()), cx);
});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
r#"fn main«1()1» «1{
let v: Vec«2<String>2» = vec!«2[]2»;
}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
2 hsla(29.00, 54.00%, 65.88%, 1.00)
"#,
&bracket_colors_markup(&mut cx),
"After switching to Rust, <> brackets are now colorized"
);
}
#[gpui::test]
async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {

View File

@@ -20880,6 +20880,36 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
.to_string(),
);
cx.update_editor(|editor, window, cx| {
editor.move_up(&MoveUp, window, cx);
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
});
cx.assert_state_with_diff(
indoc! { "
ˇone
- two
three
five
"}
.to_string(),
);
cx.update_editor(|editor, window, cx| {
editor.move_down(&MoveDown, window, cx);
editor.move_down(&MoveDown, window, cx);
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
});
cx.assert_state_with_diff(
indoc! { "
one
- two
ˇthree
- four
five
"}
.to_string(),
);
cx.set_state(indoc! { "
one
ˇTWO
@@ -20919,6 +20949,66 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_toggling_adjacent_diff_hunks_2(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let diff_base = r#"
lineA
lineB
lineC
lineD
"#
.unindent();
cx.set_state(
&r#"
ˇlineA1
lineB
lineD
"#
.unindent(),
);
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
});
executor.run_until_parked();
cx.assert_state_with_diff(
r#"
- lineA
+ ˇlineA1
lineB
lineD
"#
.unindent(),
);
cx.update_editor(|editor, window, cx| {
editor.move_down(&MoveDown, window, cx);
editor.move_right(&MoveRight, window, cx);
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
});
executor.run_until_parked();
cx.assert_state_with_diff(
r#"
- lineA
+ lineA1
lˇineB
- lineC
lineD
"#
.unindent(),
);
}
#[gpui::test]
async fn test_edits_around_expanded_deletion_hunks(
executor: BackgroundExecutor,

View File

@@ -331,7 +331,6 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
mod wit {
wit_bindgen::generate!({
skip: ["init-extension"],
path: "./wit/since_v0.8.0",
@@ -524,6 +523,12 @@ impl wit::Guest for Component {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct LanguageServerId(String);
impl LanguageServerId {
pub fn new(value: String) -> Self {
Self(value)
}
}
impl AsRef<str> for LanguageServerId {
fn as_ref(&self) -> &str {
&self.0
@@ -540,6 +545,12 @@ impl fmt::Display for LanguageServerId {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct ContextServerId(String);
impl ContextServerId {
pub fn new(value: String) -> Self {
Self(value)
}
}
impl AsRef<str> for ContextServerId {
fn as_ref(&self) -> &str {
&self.0

View File

@@ -1,155 +0,0 @@
use gpui::{App, Context, WeakEntity, Window};
use notifications::status_toast::{StatusToast, ToastIcon};
use std::sync::Arc;
use ui::{Color, IconName, SharedString};
use util::ResultExt;
use workspace::{self, Workspace};
pub fn clone_and_open(
repo_url: SharedString,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
on_success: Arc<
dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
>,
) {
let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Select as Repository Destination".into()),
});
window
.spawn(cx, async move |cx| {
let mut paths = destination_prompt.await.ok()?.ok()??;
let mut destination_dir = paths.pop()?;
let repo_name = repo_url
.split('/')
.next_back()
.map(|name| name.strip_suffix(".git").unwrap_or(name))
.unwrap_or("repository")
.to_owned();
let clone_task = workspace
.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
let destination_dir = destination_dir.clone();
let repo_url = repo_url.clone();
cx.spawn(async move |_workspace, _cx| {
fs.git_clone(&repo_url, destination_dir.as_path()).await
})
})
.ok()?;
if let Err(error) = clone_task.await {
workspace
.update(cx, |workspace, cx| {
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
.dismiss_button(true)
});
workspace.toggle_status_toast(toast, cx);
})
.log_err();
return None;
}
let has_worktrees = workspace
.read_with(cx, |workspace, cx| {
workspace.project().read(cx).worktrees(cx).next().is_some()
})
.ok()?;
let prompt_answer = if has_worktrees {
cx.update(|window, cx| {
window.prompt(
gpui::PromptLevel::Info,
&format!("Git Clone: {}", repo_name),
None,
&["Add repo to project", "Open repo in new project"],
cx,
)
})
.ok()?
.await
.ok()?
} else {
// Don't ask if project is empty
0
};
destination_dir.push(&repo_name);
match prompt_answer {
0 => {
workspace
.update_in(cx, |workspace, window, cx| {
let create_task = workspace.project().update(cx, |project, cx| {
project.create_worktree(destination_dir.as_path(), true, cx)
});
let workspace_weak = cx.weak_entity();
let on_success = on_success.clone();
cx.spawn_in(window, async move |_window, cx| {
if create_task.await.log_err().is_some() {
workspace_weak
.update_in(cx, |workspace, window, cx| {
(on_success)(workspace, window, cx);
})
.ok();
}
})
.detach();
})
.ok()?;
}
1 => {
workspace
.update(cx, move |workspace, cx| {
let app_state = workspace.app_state().clone();
let destination_path = destination_dir.clone();
let on_success = on_success.clone();
workspace::open_new(
Default::default(),
app_state,
cx,
move |workspace, window, cx| {
cx.activate(true);
let create_task =
workspace.project().update(cx, |project, cx| {
project.create_worktree(
destination_path.as_path(),
true,
cx,
)
});
let workspace_weak = cx.weak_entity();
cx.spawn_in(window, async move |_window, cx| {
if create_task.await.log_err().is_some() {
workspace_weak
.update_in(cx, |workspace, window, cx| {
(on_success)(workspace, window, cx);
})
.ok();
}
})
.detach();
},
)
.detach();
})
.ok();
}
_ => {}
}
Some(())
})
.detach();
}

View File

@@ -2848,15 +2848,93 @@ impl GitPanel {
}
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
let path = cx.prompt_for_paths(gpui::PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Select as Repository Destination".into()),
});
let workspace = self.workspace.clone();
crate::clone::clone_and_open(
repo.into(),
workspace,
window,
cx,
Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
);
cx.spawn_in(window, async move |this, cx| {
let mut paths = path.await.ok()?.ok()??;
let mut path = paths.pop()?;
let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
Ok(_) => cx.update(|window, cx| {
window.prompt(
PromptLevel::Info,
&format!("Git Clone: {}", repo_name),
None,
&["Add repo to project", "Open repo in new project"],
cx,
)
}),
Err(e) => {
this.update(cx, |this: &mut GitPanel, cx| {
let toast = StatusToast::new(e.to_string(), cx, |this, _| {
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
.dismiss_button(true)
});
this.workspace
.update(cx, |workspace, cx| {
workspace.toggle_status_toast(toast, cx);
})
.ok();
})
.ok()?;
return None;
}
}
.ok()?;
path.push(repo_name);
match prompt_answer.await.ok()? {
0 => {
workspace
.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| {
project.create_worktree(path.as_path(), true, cx)
})
.detach();
})
.ok();
}
1 => {
workspace
.update(cx, move |workspace, cx| {
workspace::open_new(
Default::default(),
workspace.app_state().clone(),
cx,
move |workspace, _, cx| {
cx.activate(true);
workspace
.project()
.update(cx, |project, cx| {
project.create_worktree(&path, true, cx)
})
.detach();
},
)
.detach();
})
.ok();
}
_ => {}
}
Some(())
})
.detach();
}
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {

View File

@@ -10,7 +10,6 @@ use ui::{
};
mod blame_ui;
pub mod clone;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},

View File

@@ -566,22 +566,22 @@ impl Model {
pub fn max_token_count(&self) -> u64 {
match self {
Self::Gemini25FlashLite => 1_048_576,
Self::Gemini25Flash => 1_048_576,
Self::Gemini25Pro => 1_048_576,
Self::Gemini3Pro => 1_048_576,
Self::Gemini3Flash => 1_048_576,
Self::Gemini25FlashLite
| Self::Gemini25Flash
| Self::Gemini25Pro
| Self::Gemini3Pro
| Self::Gemini3Flash => 1_048_576,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
pub fn max_output_tokens(&self) -> Option<u64> {
match self {
Model::Gemini25FlashLite => Some(65_536),
Model::Gemini25Flash => Some(65_536),
Model::Gemini25Pro => Some(65_536),
Model::Gemini3Pro => Some(65_536),
Model::Gemini3Flash => Some(65_536),
Model::Gemini25FlashLite
| Model::Gemini25Flash
| Model::Gemini25Pro
| Model::Gemini3Pro
| Model::Gemini3Flash => Some(65_536),
Model::Custom { .. } => None,
}
}

View File

@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
"client_system",
"dlopen",
], optional = true }
wayland-client = { version = "0.31.2", optional = true }
wayland-cursor = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.31.2", features = [
wayland-client = { version = "0.31.11", optional = true }
wayland-cursor = { version = "0.31.11", optional = true }
wayland-protocols = { version = "0.32.9", features = [
"client",
"staging",
"unstable",
], optional = true }
wayland-protocols-plasma = { version = "0.2.0", features = [
wayland-protocols-plasma = { version = "0.3.9", features = [
"client",
], optional = true }
wayland-protocols-wlr = { version = "0.3.9", features = [

View File

@@ -5,6 +5,7 @@ use gpui::{
struct SubWindow {
custom_titlebar: bool,
is_dialog: bool,
}
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
@@ -23,7 +24,10 @@ fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> imp
}
impl Render for SubWindow {
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_bounds =
WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
div()
.flex()
.flex_col()
@@ -52,8 +56,28 @@ impl Render for SubWindow {
.child(
div()
.p_8()
.flex()
.flex_col()
.gap_2()
.child("SubWindow")
.when(self.is_dialog, |div| {
div.child(button("Open Nested Dialog", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
kind: WindowKind::Dialog,
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: true,
})
},
)
.unwrap();
}))
})
.child(button("Close", |window, _| {
window.remove_window();
})),
@@ -86,6 +110,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
@@ -101,6 +126,39 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
.unwrap();
}))
.child(button("Floating", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
kind: WindowKind::Floating,
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
.unwrap();
}))
.child(button("Dialog", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
kind: WindowKind::Dialog,
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: true,
})
},
)
@@ -116,6 +174,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: true,
is_dialog: false,
})
},
)
@@ -131,6 +190,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
@@ -147,6 +207,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
@@ -162,6 +223,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
@@ -177,6 +239,7 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)

View File

@@ -28,6 +28,8 @@ pub use entity_map::*;
use http_client::{HttpClient, Url};
use smallvec::SmallVec;
#[cfg(any(test, feature = "test-support"))]
pub use test_app::*;
#[cfg(any(test, feature = "test-support"))]
pub use test_context::*;
use util::{ResultExt, debug_panic};
@@ -51,6 +53,8 @@ mod async_context;
mod context;
mod entity_map;
#[cfg(any(test, feature = "test-support"))]
mod test_app;
#[cfg(any(test, feature = "test-support"))]
mod test_context;
/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits.

View File

@@ -0,0 +1,605 @@
//! A clean testing API for GPUI applications.
//!
//! `TestApp` provides a simpler alternative to `TestAppContext` with:
//! - Automatic effect flushing after updates
//! - Clean window creation and inspection
//! - Input simulation helpers
//!
//! # Example
//! ```ignore
//! #[test]
//! fn test_my_view() {
//! let mut app = TestApp::new();
//!
//! let mut window = app.open_window(|window, cx| {
//! MyView::new(window, cx)
//! });
//!
//! window.update(|view, window, cx| {
//! view.do_something(cx);
//! });
//!
//! // Check rendered state
//! assert_eq!(window.title(), Some("Expected Title"));
//! }
//! ```
use crate::{
AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext,
Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
SceneSnapshot, Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds,
WindowHandle, WindowOptions, app::GpuiMode,
};
use rand::{SeedableRng, rngs::StdRng};
use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
/// A test application context with a clean API.
///
/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after
/// each update and provides simpler window management.
pub struct TestApp {
app: Rc<AppCell>,
platform: Rc<TestPlatform>,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
#[allow(dead_code)]
dispatcher: TestDispatcher,
text_system: Arc<TextSystem>,
}
impl TestApp {
/// Create a new test application.
pub fn new() -> Self {
Self::with_seed(0)
}
/// Create a new test application with a specific random seed.
pub fn with_seed(seed: u64) -> Self {
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
let arc_dispatcher = Arc::new(dispatcher.clone());
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
let asset_source = Arc::new(());
let http_client = http_client::FakeHttpClient::with_404_response();
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let mut app = App::new_app(platform.clone(), asset_source, http_client);
app.borrow_mut().mode = GpuiMode::test();
Self {
app,
platform,
background_executor,
foreground_executor,
dispatcher,
text_system,
}
}
/// Run a closure with mutable access to the App context.
/// Automatically runs until parked after the closure completes.
pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
let result = {
let mut app = self.app.borrow_mut();
app.update(f)
};
self.run_until_parked();
result
}
/// Run a closure with read-only access to the App context.
pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
let app = self.app.borrow();
f(&app)
}
/// Create a new entity in the app.
pub fn new_entity<T: 'static>(
&mut self,
build: impl FnOnce(&mut Context<T>) -> T,
) -> Entity<T> {
self.update(|cx| cx.new(build))
}
/// Update an entity.
pub fn update_entity<T: 'static, R>(
&mut self,
entity: &Entity<T>,
f: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> R {
self.update(|cx| entity.update(cx, f))
}
/// Read an entity.
pub fn read_entity<T: 'static, R>(
&self,
entity: &Entity<T>,
f: impl FnOnce(&T, &App) -> R,
) -> R {
self.read(|cx| f(entity.read(cx), cx))
}
/// Open a test window with the given root view.
pub fn open_window<V: Render + 'static>(
&mut self,
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
) -> TestWindow<V> {
let bounds = self.read(|cx| Bounds::maximized(None, cx));
let handle = self.update(|cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| build_view(window, cx)),
)
.unwrap()
});
TestWindow {
handle,
app: self.app.clone(),
platform: self.platform.clone(),
background_executor: self.background_executor.clone(),
}
}
/// Open a test window with specific options.
pub fn open_window_with_options<V: Render + 'static>(
&mut self,
options: WindowOptions,
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
) -> TestWindow<V> {
let handle = self.update(|cx| {
cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))
.unwrap()
});
TestWindow {
handle,
app: self.app.clone(),
platform: self.platform.clone(),
background_executor: self.background_executor.clone(),
}
}
/// Run pending tasks until there's nothing left to do.
pub fn run_until_parked(&self) {
self.background_executor.run_until_parked();
}
/// Advance the simulated clock by the given duration.
pub fn advance_clock(&self, duration: Duration) {
self.background_executor.advance_clock(duration);
}
/// Spawn a future on the foreground executor.
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
R: 'static,
{
self.foreground_executor.spawn(f(self.to_async()))
}
/// Spawn a future on the background executor.
pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.background_executor.spawn(future)
}
/// Get an async handle to the app.
pub fn to_async(&self) -> AsyncApp {
AsyncApp {
app: Rc::downgrade(&self.app),
background_executor: self.background_executor.clone(),
foreground_executor: self.foreground_executor.clone(),
}
}
/// Get the background executor.
pub fn background_executor(&self) -> &BackgroundExecutor {
&self.background_executor
}
/// Get the foreground executor.
pub fn foreground_executor(&self) -> &ForegroundExecutor {
&self.foreground_executor
}
/// Get the text system.
pub fn text_system(&self) -> &Arc<TextSystem> {
&self.text_system
}
/// Check if a global of the given type exists.
pub fn has_global<G: Global>(&self) -> bool {
self.read(|cx| cx.has_global::<G>())
}
/// Set a global value.
pub fn set_global<G: Global>(&mut self, global: G) {
self.update(|cx| cx.set_global(global));
}
/// Read a global value.
pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
self.read(|cx| f(cx.global(), cx))
}
/// Update a global value.
pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
self.update(|cx| cx.update_global(f))
}
// Platform simulation methods
/// Write text to the simulated clipboard.
pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.platform.write_to_clipboard(item);
}
/// Read from the simulated clipboard.
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.platform.read_from_clipboard()
}
/// Get URLs that have been opened via `cx.open_url()`.
pub fn opened_url(&self) -> Option<String> {
self.platform.opened_url.borrow().clone()
}
/// Check if a file path prompt is pending.
pub fn did_prompt_for_new_path(&self) -> bool {
self.platform.did_prompt_for_new_path()
}
/// Simulate answering a path selection dialog.
pub fn simulate_new_path_selection(
&self,
select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
) {
self.platform.simulate_new_path_selection(select);
}
/// Check if a prompt dialog is pending.
pub fn has_pending_prompt(&self) -> bool {
self.platform.has_pending_prompt()
}
/// Simulate answering a prompt dialog.
pub fn simulate_prompt_answer(&self, button: &str) {
self.platform.simulate_prompt_answer(button);
}
/// Get all open windows.
pub fn windows(&self) -> Vec<AnyWindowHandle> {
self.read(|cx| cx.windows())
}
}
impl Default for TestApp {
fn default() -> Self {
Self::new()
}
}
/// A test window with inspection and simulation capabilities.
pub struct TestWindow<V> {
handle: WindowHandle<V>,
app: Rc<AppCell>,
platform: Rc<TestPlatform>,
background_executor: BackgroundExecutor,
}
impl<V: 'static + Render> TestWindow<V> {
/// Get the window handle.
pub fn handle(&self) -> WindowHandle<V> {
self.handle
}
/// Get the root view entity.
pub fn root(&self) -> Entity<V> {
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |root_view, _, _| {
root_view.downcast::<V>().expect("root view type mismatch")
})
.expect("window not found")
}
/// Update the root view.
/// Automatically draws the window after the update to ensure the scene is current.
pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
let result = {
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |root_view, window, cx| {
let view = root_view.downcast::<V>().expect("root view type mismatch");
view.update(cx, |view, cx| f(view, window, cx))
})
.expect("window not found")
};
self.background_executor.run_until_parked();
self.draw();
result
}
/// Read the root view.
pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
let app = self.app.borrow();
let view = self
.app
.borrow()
.windows
.get(self.handle.window_id())
.and_then(|w| w.as_ref())
.and_then(|w| w.root.clone())
.and_then(|r| r.downcast::<V>().ok())
.expect("window or root view not found");
f(view.read(&app), &app)
}
/// Get the window title.
pub fn title(&self) -> Option<String> {
let app = self.app.borrow();
app.read_window(&self.handle, |_, _cx| {
// TODO: expose title through Window API
None
})
.unwrap()
}
/// Simulate a keystroke.
/// Automatically draws the window after the keystroke.
pub fn simulate_keystroke(&mut self, keystroke: &str) {
let keystroke = Keystroke::parse(keystroke).unwrap();
{
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |_, window, cx| {
window.dispatch_keystroke(keystroke, cx);
})
.unwrap();
}
self.background_executor.run_until_parked();
self.draw();
}
/// Simulate multiple keystrokes (space-separated).
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
for keystroke in keystrokes.split(' ') {
self.simulate_keystroke(keystroke);
}
}
/// Simulate typing text.
pub fn simulate_input(&mut self, input: &str) {
for char in input.chars() {
self.simulate_keystroke(&char.to_string());
}
}
/// Simulate a mouse move.
pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
self.simulate_event(MouseMoveEvent {
position,
modifiers: Default::default(),
pressed_button: None,
});
}
/// Simulate a mouse down event.
pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
self.simulate_event(MouseDownEvent {
position,
button,
modifiers: Default::default(),
click_count: 1,
first_mouse: false,
});
}
/// Simulate a mouse up event.
pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
self.simulate_event(MouseUpEvent {
position,
button,
modifiers: Default::default(),
click_count: 1,
});
}
/// Simulate a click at the given position.
pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
self.simulate_mouse_down(position, button);
self.simulate_mouse_up(position, button);
}
/// Simulate a scroll event.
pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
self.simulate_event(crate::ScrollWheelEvent {
position,
delta: crate::ScrollDelta::Pixels(delta),
modifiers: Default::default(),
touch_phase: crate::TouchPhase::Moved,
});
}
/// Simulate an input event.
/// Automatically draws the window after the event.
pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
let platform_input = event.to_platform_input();
{
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |_, window, cx| {
window.dispatch_event(platform_input, cx);
})
.unwrap();
}
self.background_executor.run_until_parked();
self.draw();
}
/// Simulate resizing the window.
/// Automatically draws the window after the resize.
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
let window_id = self.handle.window_id();
let mut app = self.app.borrow_mut();
if let Some(Some(window)) = app.windows.get_mut(window_id) {
if let Some(test_window) = window.platform_window.as_test() {
test_window.simulate_resize(size);
}
}
drop(app);
self.background_executor.run_until_parked();
self.draw();
}
/// Force a redraw of the window.
pub fn draw(&mut self) {
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |_, window, cx| {
window.draw(cx).clear();
})
.unwrap();
}
/// Get a snapshot of the rendered scene for inspection.
/// The scene is automatically kept up to date after `update()` and `simulate_*()` calls.
pub fn scene_snapshot(&self) -> SceneSnapshot {
let app = self.app.borrow();
let window = app
.windows
.get(self.handle.window_id())
.and_then(|w| w.as_ref())
.expect("window not found");
window.rendered_frame.scene.snapshot()
}
/// Get the named diagnostic quads recorded during imperative paint, without inspecting the
/// rest of the scene snapshot.
///
/// This is useful for tests that want a stable, semantic view of layout/paint geometry without
/// coupling to the low-level quad/glyph output.
pub fn diagnostic_quads(&self) -> Vec<crate::scene::test_scene::DiagnosticQuad> {
self.scene_snapshot().diagnostic_quads
}
}
impl<V> Clone for TestWindow<V> {
fn clone(&self) -> Self {
Self {
handle: self.handle,
app: self.app.clone(),
platform: self.platform.clone(),
background_executor: self.background_executor.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FocusHandle, Focusable, div, prelude::*};
struct Counter {
count: usize,
focus_handle: FocusHandle,
}
impl Counter {
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
Self {
count: 0,
focus_handle,
}
}
fn increment(&mut self, _cx: &mut Context<Self>) {
self.count += 1;
}
}
impl Focusable for Counter {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Counter {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().child(format!("Count: {}", self.count))
}
}
#[test]
fn test_basic_usage() {
let mut app = TestApp::new();
let mut window = app.open_window(Counter::new);
window.update(|counter, _window, cx| {
counter.increment(cx);
});
window.read(|counter, _| {
assert_eq!(counter.count, 1);
});
}
#[test]
fn test_entity_creation() {
let mut app = TestApp::new();
let entity = app.new_entity(|cx| Counter {
count: 42,
focus_handle: cx.focus_handle(),
});
app.read_entity(&entity, |counter, _| {
assert_eq!(counter.count, 42);
});
app.update_entity(&entity, |counter, _cx| {
counter.count += 1;
});
app.read_entity(&entity, |counter, _| {
assert_eq!(counter.count, 43);
});
}
#[test]
fn test_globals() {
let mut app = TestApp::new();
struct MyGlobal(String);
impl Global for MyGlobal {}
assert!(!app.has_global::<MyGlobal>());
app.set_global(MyGlobal("hello".into()));
assert!(app.has_global::<MyGlobal>());
app.read_global::<MyGlobal, _>(|global, _| {
assert_eq!(global.0, "hello");
});
app.update_global::<MyGlobal, _>(|global, _| {
global.0 = "world".into();
});
app.read_global::<MyGlobal, _>(|global, _| {
assert_eq!(global.0, "world");
});
}
}

View File

@@ -3,9 +3,9 @@ use crate::{
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
WindowHandle, WindowOptions, app::GpuiMode,
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestPlatformWindow,
TestScreenCaptureSource, TextSystem, VisualContext, Window, WindowBounds, WindowHandle,
WindowOptions, app::GpuiMode,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt, channel::oneshot};
@@ -220,7 +220,7 @@ impl TestAppContext {
f(&cx)
}
/// Adds a new window. The Window will always be backed by a `TestWindow` which
/// Adds a new window. The Window will always be backed by a `TestPlatformWindow` which
/// can be retrieved with `self.test_window(handle)`
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
where
@@ -465,8 +465,8 @@ impl TestAppContext {
.unwrap();
}
/// Returns the `TestWindow` backing the given handle.
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
/// Returns the `TestPlatformWindow` backing the given handle.
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestPlatformWindow {
self.app
.borrow_mut()
.windows

View File

@@ -808,6 +808,15 @@ impl LinearColorStop {
}
impl Background {
/// Returns the solid color if this is a solid background, None otherwise.
pub fn as_solid(&self) -> Option<Hsla> {
if self.tag == BackgroundTag::Solid {
Some(self.solid)
} else {
None
}
}
/// Use specified color space for color interpolation.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>

View File

@@ -561,7 +561,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
None
}
}
@@ -1348,6 +1348,10 @@ pub enum WindowKind {
/// docks, notifications or wallpapers.
#[cfg(all(target_os = "linux", feature = "wayland"))]
LayerShell(layer_shell::LayerShellOptions),
/// A window that appears on top of its parent window and blocks interaction with it
/// until the modal window is closed
Dialog,
}
/// The appearance of the window, as defined by the operating system.

View File

@@ -36,12 +36,6 @@ use wayland_client::{
wl_shm_pool, wl_surface,
},
};
use wayland_protocols::wp::cursor_shape::v1::client::{
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
};
use wayland_protocols::wp::fractional_scale::v1::client::{
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
self, ZwpPrimarySelectionOfferV1,
};
@@ -61,6 +55,14 @@ use wayland_protocols::xdg::decoration::zv1::client::{
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
use wayland_protocols::{
wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
};
use wayland_protocols::{
wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1},
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
};
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
@@ -122,6 +124,7 @@ pub struct Globals {
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
pub executor: ForegroundExecutor,
}
@@ -132,6 +135,7 @@ impl Globals {
qh: QueueHandle<WaylandClientStatePtr>,
seat: wl_seat::WlSeat,
) -> Self {
let dialog_v = XdgWmDialogV1::interface().version;
Globals {
activation: globals.bind(&qh, 1..=1, ()).ok(),
compositor: globals
@@ -160,6 +164,7 @@ impl Globals {
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
executor,
qh,
}
@@ -729,10 +734,7 @@ impl LinuxClient for WaylandClient {
) -> anyhow::Result<Box<dyn PlatformWindow>> {
let mut state = self.0.borrow_mut();
let parent = state
.keyboard_focused_window
.as_ref()
.and_then(|w| w.toplevel());
let parent = state.keyboard_focused_window.clone();
let (window, surface_id) = WaylandWindow::new(
handle,
@@ -751,7 +753,12 @@ impl LinuxClient for WaylandClient {
fn set_cursor_style(&self, style: CursorStyle) {
let mut state = self.0.borrow_mut();
let need_update = state.cursor_style != Some(style);
let need_update = state.cursor_style != Some(style)
&& (state.mouse_focused_window.is_none()
|| state
.mouse_focused_window
.as_ref()
.is_some_and(|w| !w.is_blocked()));
if need_update {
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
@@ -1011,7 +1018,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
}
}
fn get_window(
pub(crate) fn get_window(
mut state: &mut RefMut<WaylandClientState>,
surface_id: &ObjectId,
) -> Option<WaylandWindowStatePtr> {
@@ -1654,6 +1661,30 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
if let Some(window) = state.mouse_focused_window.clone() {
if window.is_blocked() {
let default_style = CursorStyle::Arrow;
if state.cursor_style != Some(default_style) {
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
state.cursor_style = Some(default_style);
if let Some(cursor_shape_device) = &state.cursor_shape_device {
cursor_shape_device.set_shape(serial, default_style.to_shape());
} else {
// cursor-shape-v1 isn't supported, set the cursor using a surface.
let wl_pointer = state
.wl_pointer
.clone()
.expect("window is focused by pointer");
let scale = window.primary_output_scale();
state.cursor.set_icon(
&wl_pointer,
serial,
default_style.to_icon_names(),
scale,
);
}
}
}
if state
.keyboard_focused_window
.as_ref()
@@ -2225,3 +2256,27 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
}
}
}
impl Dispatch<XdgWmDialogV1, ()> for WaylandClientStatePtr {
fn event(
_: &mut Self,
_: &XdgWmDialogV1,
_: <XdgWmDialogV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<XdgDialogV1, ()> for WaylandClientStatePtr {
fn event(
_state: &mut Self,
_proxy: &XdgDialogV1,
_event: <XdgDialogV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}

View File

@@ -7,7 +7,7 @@ use std::{
};
use blade_graphics as gpu;
use collections::HashMap;
use collections::{FxHashSet, HashMap};
use futures::channel::oneshot::Receiver;
use raw_window_handle as rwh;
@@ -20,7 +20,7 @@ use wayland_protocols::xdg::shell::client::xdg_surface;
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
use wayland_protocols::{
wp::fractional_scale::v1::client::wp_fractional_scale_v1,
xdg::shell::client::xdg_toplevel::XdgToplevel,
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
};
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
@@ -29,7 +29,7 @@ use crate::{
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, get_window,
layer_shell::LayerShellNotSupportedError, px, size,
};
use crate::{
@@ -87,6 +87,8 @@ struct InProgressConfigure {
pub struct WaylandWindowState {
surface_state: WaylandSurfaceState,
acknowledged_first_configure: bool,
parent: Option<WaylandWindowStatePtr>,
children: FxHashSet<ObjectId>,
pub surface: wl_surface::WlSurface,
app_id: Option<String>,
appearance: WindowAppearance,
@@ -126,7 +128,7 @@ impl WaylandSurfaceState {
surface: &wl_surface::WlSurface,
globals: &Globals,
params: &WindowParams,
parent: Option<XdgToplevel>,
parent: Option<WaylandWindowStatePtr>,
) -> anyhow::Result<Self> {
// For layer_shell windows, create a layer surface instead of an xdg surface
if let WindowKind::LayerShell(options) = &params.kind {
@@ -178,10 +180,28 @@ impl WaylandSurfaceState {
.get_xdg_surface(&surface, &globals.qh, surface.id());
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
if params.kind == WindowKind::Floating {
toplevel.set_parent(parent.as_ref());
let xdg_parent = parent.as_ref().and_then(|w| w.toplevel());
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
toplevel.set_parent(xdg_parent.as_ref());
}
let dialog = if params.kind == WindowKind::Dialog {
let dialog = globals.dialog.as_ref().map(|dialog| {
let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ());
xdg_dialog.set_modal();
xdg_dialog
});
if let Some(parent) = parent.as_ref() {
parent.add_child(surface.id());
}
dialog
} else {
None
};
if let Some(size) = params.window_min_size {
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
}
@@ -198,6 +218,7 @@ impl WaylandSurfaceState {
xdg_surface,
toplevel,
decoration,
dialog,
}))
}
}
@@ -206,6 +227,7 @@ pub struct WaylandXdgSurfaceState {
xdg_surface: xdg_surface::XdgSurface,
toplevel: xdg_toplevel::XdgToplevel,
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
dialog: Option<XdgDialogV1>,
}
pub struct WaylandLayerSurfaceState {
@@ -258,7 +280,13 @@ impl WaylandSurfaceState {
xdg_surface,
toplevel,
decoration: _decoration,
dialog,
}) => {
// drop the dialog before toplevel so compositor can explicitly unapply it's effects
if let Some(dialog) = dialog {
dialog.destroy();
}
// The role object (toplevel) must always be destroyed before the xdg_surface.
// See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
toplevel.destroy();
@@ -288,6 +316,7 @@ impl WaylandWindowState {
globals: Globals,
gpu_context: &BladeContext,
options: WindowParams,
parent: Option<WaylandWindowStatePtr>,
) -> anyhow::Result<Self> {
let renderer = {
let raw_window = RawWindow {
@@ -319,6 +348,8 @@ impl WaylandWindowState {
Ok(Self {
surface_state,
acknowledged_first_configure: false,
parent,
children: FxHashSet::default(),
surface,
app_id: None,
blur: None,
@@ -391,6 +422,10 @@ impl Drop for WaylandWindow {
fn drop(&mut self) {
let mut state = self.0.state.borrow_mut();
let surface_id = state.surface.id();
if let Some(parent) = state.parent.as_ref() {
parent.state.borrow_mut().children.remove(&surface_id);
}
let client = state.client.clone();
state.renderer.destroy();
@@ -448,10 +483,10 @@ impl WaylandWindow {
client: WaylandClientStatePtr,
params: WindowParams,
appearance: WindowAppearance,
parent: Option<XdgToplevel>,
parent: Option<WaylandWindowStatePtr>,
) -> anyhow::Result<(Self, ObjectId)> {
let surface = globals.compositor.create_surface(&globals.qh, ());
let surface_state = WaylandSurfaceState::new(&surface, &globals, &params, parent)?;
let surface_state = WaylandSurfaceState::new(&surface, &globals, &params, parent.clone())?;
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@@ -473,6 +508,7 @@ impl WaylandWindow {
globals,
gpu_context,
params,
parent,
)?)),
callbacks: Rc::new(RefCell::new(Callbacks::default())),
});
@@ -501,6 +537,16 @@ impl WaylandWindowStatePtr {
Rc::ptr_eq(&self.state, &other.state)
}
pub fn add_child(&self, child: ObjectId) {
let mut state = self.state.borrow_mut();
state.children.insert(child);
}
pub fn is_blocked(&self) -> bool {
let state = self.state.borrow();
!state.children.is_empty()
}
pub fn frame(&self) {
let mut state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id());
@@ -818,6 +864,9 @@ impl WaylandWindowStatePtr {
}
pub fn handle_ime(&self, ime: ImeInput) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -894,6 +943,21 @@ impl WaylandWindowStatePtr {
}
pub fn close(&self) {
let state = self.state.borrow();
let client = state.client.get_client();
#[allow(clippy::mutable_key_type)]
let children = state.children.clone();
drop(state);
for child in children {
let mut client_state = client.borrow_mut();
let window = get_window(&mut client_state, &child);
drop(client_state);
if let Some(child) = window {
child.close();
}
}
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
fun()
@@ -901,6 +965,9 @@ impl WaylandWindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
if self.is_blocked() {
return;
}
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
&& !fun(input.clone()).propagate
{

View File

@@ -222,7 +222,7 @@ pub struct X11ClientState {
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
impl X11ClientStatePtr {
fn get_client(&self) -> Option<X11Client> {
pub fn get_client(&self) -> Option<X11Client> {
self.0.upgrade().map(X11Client)
}
@@ -752,7 +752,7 @@ impl X11Client {
}
}
fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
pub(crate) fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
let state = self.0.borrow();
state
.windows
@@ -789,12 +789,12 @@ impl X11Client {
let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
let mut state = self.0.borrow_mut();
if atom == state.atoms.WM_DELETE_WINDOW {
if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() {
// window "x" button clicked by user
if window.should_close() {
// Rest of the close logic is handled in drop_window()
window.close();
}
// Rest of the close logic is handled in drop_window()
drop(state);
window.close();
state = self.0.borrow_mut();
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
window.state.borrow_mut().last_sync_counter =
Some(x11rb::protocol::sync::Int64 {
@@ -1216,6 +1216,33 @@ impl X11Client {
Event::XinputMotion(event) => {
let window = self.get_window(event.event)?;
let mut state = self.0.borrow_mut();
if window.is_blocked() {
// We want to set the cursor to the default arrow
// when the window is blocked
let style = CursorStyle::Arrow;
let current_style = state
.cursor_styles
.get(&window.x_window)
.unwrap_or(&CursorStyle::Arrow);
if *current_style != style
&& let Some(cursor) = state.get_cursor_icon(style)
{
state.cursor_styles.insert(window.x_window, style);
check_reply(
|| "Failed to set cursor style",
state.xcb_connection.change_window_attributes(
window.x_window,
&ChangeWindowAttributesAux {
cursor: Some(cursor),
..Default::default()
},
),
)
.log_err();
state.xcb_connection.flush().log_err();
};
}
let pressed_button = pressed_button_from_mask(event.button_mask[0]);
let position = point(
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
@@ -1489,7 +1516,7 @@ impl LinuxClient for X11Client {
let parent_window = state
.keyboard_focused_window
.and_then(|focused_window| state.windows.get(&focused_window))
.map(|window| window.window.x_window);
.map(|w| w.window.clone());
let x_window = state
.xcb_connection
.generate_id()
@@ -1544,7 +1571,15 @@ impl LinuxClient for X11Client {
.cursor_styles
.get(&focused_window)
.unwrap_or(&CursorStyle::Arrow);
if *current_style == style {
let window = state
.mouse_focused_window
.and_then(|w| state.windows.get(&w));
let should_change = *current_style != style
&& (window.is_none() || window.is_some_and(|w| !w.is_blocked()));
if !should_change {
return;
}

View File

@@ -11,6 +11,7 @@ use crate::{
};
use blade_graphics as gpu;
use collections::FxHashSet;
use raw_window_handle as rwh;
use util::{ResultExt, maybe};
use x11rb::{
@@ -74,6 +75,7 @@ x11rb::atom_manager! {
_NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_NOTIFICATION,
_NET_WM_WINDOW_TYPE_DIALOG,
_NET_WM_STATE_MODAL,
_NET_WM_SYNC,
_NET_SUPPORTED,
_MOTIF_WM_HINTS,
@@ -249,6 +251,8 @@ pub struct Callbacks {
pub struct X11WindowState {
pub destroyed: bool,
parent: Option<X11WindowStatePtr>,
children: FxHashSet<xproto::Window>,
client: X11ClientStatePtr,
executor: ForegroundExecutor,
atoms: XcbAtoms,
@@ -394,7 +398,7 @@ impl X11WindowState {
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
parent_window: Option<xproto::Window>,
parent_window: Option<X11WindowStatePtr>,
) -> anyhow::Result<Self> {
let x_screen_index = params
.display_id
@@ -546,8 +550,8 @@ impl X11WindowState {
)?;
}
if params.kind == WindowKind::Floating {
if let Some(parent_window) = parent_window {
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) {
// WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
// a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
// place the floating window in relation to the main window.
@@ -563,11 +567,23 @@ impl X11WindowState {
),
)?;
}
}
let parent = if params.kind == WindowKind::Dialog
&& let Some(parent) = parent_window
{
parent.add_child(x_window);
Some(parent)
} else {
None
};
if params.kind == WindowKind::Dialog {
// _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
// https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
check_reply(
|| "X11 ChangeProperty32 setting window type for floating window failed.",
|| "X11 ChangeProperty32 setting window type for dialog window failed.",
xcb.change_property32(
xproto::PropMode::REPLACE,
x_window,
@@ -576,6 +592,20 @@ impl X11WindowState {
&[atoms._NET_WM_WINDOW_TYPE_DIALOG],
),
)?;
// We set the modal state for dialog windows, so that the window manager
// can handle it appropriately (e.g., prevent interaction with the parent window
// while the dialog is open).
check_reply(
|| "X11 ChangeProperty32 setting modal state for dialog window failed.",
xcb.change_property32(
xproto::PropMode::REPLACE,
x_window,
atoms._NET_WM_STATE,
xproto::AtomEnum::ATOM,
&[atoms._NET_WM_STATE_MODAL],
),
)?;
}
check_reply(
@@ -667,6 +697,8 @@ impl X11WindowState {
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
Ok(Self {
parent,
children: FxHashSet::default(),
client,
executor,
display,
@@ -720,6 +752,11 @@ pub(crate) struct X11Window(pub X11WindowStatePtr);
impl Drop for X11Window {
fn drop(&mut self) {
let mut state = self.0.state.borrow_mut();
if let Some(parent) = state.parent.as_ref() {
parent.state.borrow_mut().children.remove(&self.0.x_window);
}
state.renderer.destroy();
let destroy_x_window = maybe!({
@@ -734,8 +771,6 @@ impl Drop for X11Window {
.log_err();
if destroy_x_window.is_some() {
// Mark window as destroyed so that we can filter out when X11 events
// for it still come in.
state.destroyed = true;
let this_ptr = self.0.clone();
@@ -773,7 +808,7 @@ impl X11Window {
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
parent_window: Option<xproto::Window>,
parent_window: Option<X11WindowStatePtr>,
) -> anyhow::Result<Self> {
let ptr = X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new(
@@ -979,7 +1014,31 @@ impl X11WindowStatePtr {
Ok(())
}
pub fn add_child(&self, child: xproto::Window) {
let mut state = self.state.borrow_mut();
state.children.insert(child);
}
pub fn is_blocked(&self) -> bool {
let state = self.state.borrow();
!state.children.is_empty()
}
pub fn close(&self) {
let state = self.state.borrow();
let client = state.client.clone();
#[allow(clippy::mutable_key_type)]
let children = state.children.clone();
drop(state);
if let Some(client) = client.get_client() {
for child in children {
if let Some(child_window) = client.get_window(child) {
child_window.close();
}
}
}
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
fun()
@@ -994,6 +1053,9 @@ impl X11WindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
if self.is_blocked() {
return;
}
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
&& !fun(input.clone()).propagate
{
@@ -1016,6 +1078,9 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_commit(&self, text: String) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1026,6 +1091,9 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_preedit(&self, text: String) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1036,6 +1104,9 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_unmark(&self) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1046,6 +1117,9 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_delete(&self) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);

View File

@@ -62,9 +62,12 @@ static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
NSWindowStyleMask::from_bits_retain(1 << 7);
// WindowLevel const value ref: https://docs.rs/core-graphics2/0.4.1/src/core_graphics2/window_level.rs.html
#[allow(non_upper_case_globals)]
const NSNormalWindowLevel: NSInteger = 0;
#[allow(non_upper_case_globals)]
const NSFloatingWindowLevel: NSInteger = 3;
#[allow(non_upper_case_globals)]
const NSPopUpWindowLevel: NSInteger = 101;
#[allow(non_upper_case_globals)]
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
@@ -423,6 +426,8 @@ struct MacWindowState {
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
activated_least_once: bool,
// The parent window if this window is a sheet (Dialog kind)
sheet_parent: Option<id>,
}
impl MacWindowState {
@@ -622,11 +627,16 @@ impl MacWindow {
}
let native_window: id = match kind {
WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
WindowKind::Normal => {
msg_send![WINDOW_CLASS, alloc]
}
WindowKind::PopUp => {
style_mask |= NSWindowStyleMaskNonactivatingPanel;
msg_send![PANEL_CLASS, alloc]
}
WindowKind::Floating | WindowKind::Dialog => {
msg_send![PANEL_CLASS, alloc]
}
};
let display = display_id
@@ -729,6 +739,7 @@ impl MacWindow {
select_previous_tab_callback: None,
toggle_tab_bar_callback: None,
activated_least_once: false,
sheet_parent: None,
})));
(*native_window).set_ivar(
@@ -779,9 +790,18 @@ impl MacWindow {
content_view.addSubview_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
let app: id = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow];
let mut sheet_parent = None;
match kind {
WindowKind::Normal | WindowKind::Floating => {
native_window.setLevel_(NSNormalWindowLevel);
if kind == WindowKind::Floating {
// Let the window float keep above normal windows.
native_window.setLevel_(NSFloatingWindowLevel);
} else {
native_window.setLevel_(NSNormalWindowLevel);
}
native_window.setAcceptsMouseMovedEvents_(YES);
if let Some(tabbing_identifier) = tabbing_identifier {
@@ -816,10 +836,23 @@ impl MacWindow {
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
);
}
WindowKind::Dialog => {
if !main_window.is_null() {
let parent = {
let active_sheet: id = msg_send![main_window, attachedSheet];
if active_sheet.is_null() {
main_window
} else {
active_sheet
}
};
let _: () =
msg_send![parent, beginSheet: native_window completionHandler: nil];
sheet_parent = Some(parent);
}
}
}
let app = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow];
if allows_automatic_window_tabbing
&& !main_window.is_null()
&& main_window != native_window
@@ -861,7 +894,11 @@ impl MacWindow {
// the window position might be incorrect if the main screen (the screen that contains the window that has focus)
// is different from the primary screen.
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
window.0.lock().move_traffic_light();
{
let mut window_state = window.0.lock();
window_state.move_traffic_light();
window_state.sheet_parent = sheet_parent;
}
pool.drain();
@@ -938,6 +975,7 @@ impl Drop for MacWindow {
let mut this = self.0.lock();
this.renderer.destroy();
let window = this.native_window;
let sheet_parent = this.sheet_parent.take();
this.display_link.take();
unsafe {
this.native_window.setDelegate_(nil);
@@ -946,6 +984,9 @@ impl Drop for MacWindow {
this.executor
.spawn(async move {
unsafe {
if let Some(parent) = sheet_parent {
let _: () = msg_send![parent, endSheet: window];
}
window.close();
window.autorelease();
}

View File

@@ -3,7 +3,7 @@ use crate::{
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
TestDisplay, TestPlatformWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -26,7 +26,7 @@ pub(crate) struct TestPlatform {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
pub(crate) active_window: RefCell<Option<TestWindow>>,
pub(crate) active_window: RefCell<Option<TestPlatformWindow>>,
active_display: Rc<dyn PlatformDisplay>,
active_cursor: Mutex<CursorStyle>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
@@ -196,7 +196,7 @@ impl TestPlatform {
rx
}
pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
pub(crate) fn set_active_window(&self, window: Option<TestPlatformWindow>) {
let executor = self.foreground_executor();
let previous_window = self.active_window.borrow_mut().take();
self.active_window.borrow_mut().clone_from(&window);
@@ -314,7 +314,7 @@ impl Platform for TestPlatform {
handle: AnyWindowHandle,
params: WindowParams,
) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
let window = TestWindow::new(
let window = TestPlatformWindow::new(
handle,
params,
self.weak.clone(),

View File

@@ -12,7 +12,7 @@ use std::{
sync::{self, Arc},
};
pub(crate) struct TestWindowState {
pub(crate) struct TestPlatformWindowState {
pub(crate) bounds: Bounds<Pixels>,
pub(crate) handle: AnyWindowHandle,
display: Rc<dyn PlatformDisplay>,
@@ -32,9 +32,9 @@ pub(crate) struct TestWindowState {
}
#[derive(Clone)]
pub(crate) struct TestWindow(pub(crate) Rc<Mutex<TestWindowState>>);
pub(crate) struct TestPlatformWindow(pub(crate) Rc<Mutex<TestPlatformWindowState>>);
impl HasWindowHandle for TestWindow {
impl HasWindowHandle for TestPlatformWindow {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
@@ -42,7 +42,7 @@ impl HasWindowHandle for TestWindow {
}
}
impl HasDisplayHandle for TestWindow {
impl HasDisplayHandle for TestPlatformWindow {
fn display_handle(
&self,
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
@@ -50,14 +50,14 @@ impl HasDisplayHandle for TestWindow {
}
}
impl TestWindow {
impl TestPlatformWindow {
pub fn new(
handle: AnyWindowHandle,
params: WindowParams,
platform: Weak<TestPlatform>,
display: Rc<dyn PlatformDisplay>,
) -> Self {
Self(Rc::new(Mutex::new(TestWindowState {
Self(Rc::new(Mutex::new(TestPlatformWindowState {
bounds: params.bounds,
display,
platform,
@@ -111,7 +111,7 @@ impl TestWindow {
}
}
impl PlatformWindow for TestWindow {
impl PlatformWindow for TestPlatformWindow {
fn bounds(&self) -> Bounds<Pixels> {
self.0.lock().bounds
}
@@ -272,7 +272,7 @@ impl PlatformWindow for TestWindow {
self.0.lock().sprite_atlas.clone()
}
fn as_test(&mut self) -> Option<&mut TestWindow> {
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
Some(self)
}

View File

@@ -40,6 +40,11 @@ impl WindowsWindowInner {
lparam: LPARAM,
) -> LRESULT {
let handled = match msg {
// eagerly activate the window, so calls to `active_window` will work correctly
WM_MOUSEACTIVATE => {
unsafe { SetActiveWindow(handle).log_err() };
None
}
WM_ACTIVATE => self.handle_activate_msg(wparam),
WM_CREATE => self.handle_create_msg(handle),
WM_MOVE => self.handle_move_msg(handle, lparam),
@@ -265,6 +270,14 @@ impl WindowsWindowInner {
fn handle_destroy_msg(&self, handle: HWND) -> Option<isize> {
let callback = { self.state.callbacks.close.take() };
// Re-enable parent window if this was a modal dialog
if let Some(parent_hwnd) = self.parent_hwnd {
unsafe {
let _ = EnableWindow(parent_hwnd, true);
let _ = SetForegroundWindow(parent_hwnd);
}
}
if let Some(callback) = callback {
callback();
}

View File

@@ -659,7 +659,7 @@ impl Platform for WindowsPlatform {
if let Err(err) = result {
// ERROR_NOT_FOUND means the credential doesn't exist.
// Return Ok(None) to match macOS and Linux behavior.
if err.code().0 == ERROR_NOT_FOUND.0 as i32 {
if err.code() == ERROR_NOT_FOUND.to_hresult() {
return Ok(None);
}
return Err(err.into());

View File

@@ -83,6 +83,7 @@ pub(crate) struct WindowsWindowInner {
pub(crate) validation_number: usize,
pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
pub(crate) platform_window_handle: HWND,
pub(crate) parent_hwnd: Option<HWND>,
}
impl WindowsWindowState {
@@ -241,6 +242,7 @@ impl WindowsWindowInner {
main_receiver: context.main_receiver.clone(),
platform_window_handle: context.platform_window_handle,
system_settings: WindowsSystemSettings::new(context.display),
parent_hwnd: context.parent_hwnd,
}))
}
@@ -368,6 +370,7 @@ struct WindowCreateContext {
disable_direct_composition: bool,
directx_devices: DirectXDevices,
invalidate_devices: Arc<AtomicBool>,
parent_hwnd: Option<HWND>,
}
impl WindowsWindow {
@@ -390,6 +393,20 @@ impl WindowsWindow {
invalidate_devices,
} = creation_info;
register_window_class(icon);
let parent_hwnd = if params.kind == WindowKind::Dialog {
let parent_window = unsafe { GetActiveWindow() };
if parent_window.is_invalid() {
None
} else {
// Disable the parent window to make this dialog modal
unsafe {
EnableWindow(parent_window, false).as_bool();
};
Some(parent_window)
}
} else {
None
};
let hide_title_bar = params
.titlebar
.as_ref()
@@ -416,8 +433,14 @@ impl WindowsWindow {
if params.is_minimizable {
dwstyle |= WS_MINIMIZEBOX;
}
let dwexstyle = if params.kind == WindowKind::Dialog {
dwstyle |= WS_POPUP | WS_CAPTION;
WS_EX_DLGMODALFRAME
} else {
WS_EX_APPWINDOW
};
(WS_EX_APPWINDOW, dwstyle)
(dwexstyle, dwstyle)
};
if !disable_direct_composition {
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
@@ -449,6 +472,7 @@ impl WindowsWindow {
disable_direct_composition,
directx_devices,
invalidate_devices,
parent_hwnd,
};
let creation_result = unsafe {
CreateWindowExW(
@@ -460,7 +484,7 @@ impl WindowsWindow {
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
None,
parent_hwnd,
None,
Some(hinstance.into()),
Some(&context as *const _ as *const _),

View File

@@ -20,6 +20,126 @@ pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
pub(crate) type DrawOrder = u32;
/// Test-only scene snapshot for inspecting rendered content.
#[cfg(any(test, feature = "test-support"))]
pub mod test_scene {
use crate::{Bounds, Hsla, Point, ScaledPixels, SharedString};
/// A rendered quad (background, border, cursor, selection, etc.)
#[derive(Debug, Clone)]
pub struct RenderedQuad {
/// Bounds in scaled pixels.
pub bounds: Bounds<ScaledPixels>,
/// Background color (if solid).
pub background_color: Option<Hsla>,
/// Border color.
pub border_color: Hsla,
}
/// A named diagnostic quad for tests and debugging of imperative paint logic.
///
/// This is not necessarily a "real" painted quad; it is metadata recorded alongside a scene.
#[derive(Debug, Clone)]
pub struct DiagnosticQuad {
/// A stable name that test code can filter by.
pub name: SharedString,
/// Bounds in scaled pixels.
pub bounds: Bounds<ScaledPixels>,
/// Optional color hint (useful when visualizing).
pub color: Option<Hsla>,
}
/// A rendered text glyph.
#[derive(Debug, Clone)]
pub struct RenderedGlyph {
/// Origin position in scaled pixels.
pub origin: Point<ScaledPixels>,
/// Size in scaled pixels.
pub size: crate::Size<ScaledPixels>,
/// Color of the glyph.
pub color: Hsla,
}
/// Snapshot of scene contents for testing.
#[derive(Debug, Default)]
pub struct SceneSnapshot {
/// All rendered quads.
pub quads: Vec<RenderedQuad>,
/// All rendered text glyphs.
pub glyphs: Vec<RenderedGlyph>,
/// Named diagnostic quads recorded by imperative drawing code for tests/debugging.
pub diagnostic_quads: Vec<DiagnosticQuad>,
/// Number of shadow primitives.
pub shadow_count: usize,
/// Number of path primitives.
pub path_count: usize,
/// Number of underline primitives.
pub underline_count: usize,
/// Number of polychrome sprites (images, emoji).
pub polychrome_sprite_count: usize,
/// Number of surface primitives.
pub surface_count: usize,
}
impl SceneSnapshot {
/// Get unique Y positions of quads, sorted.
pub fn quad_y_positions(&self) -> Vec<f32> {
let mut positions: Vec<f32> = self.quads.iter().map(|q| q.bounds.origin.y.0).collect();
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
positions.dedup();
positions
}
/// Get unique Y positions of glyphs, sorted.
pub fn glyph_y_positions(&self) -> Vec<f32> {
let mut positions: Vec<f32> = self.glyphs.iter().map(|g| g.origin.y.0).collect();
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
positions.dedup();
positions
}
/// Find quads within a Y range.
pub fn quads_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedQuad> {
self.quads
.iter()
.filter(|q| {
let y = q.bounds.origin.y.0;
y >= min_y && y < max_y
})
.collect()
}
/// Find glyphs within a Y range.
pub fn glyphs_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedGlyph> {
self.glyphs
.iter()
.filter(|g| {
let y = g.origin.y.0;
y >= min_y && y < max_y
})
.collect()
}
/// Debug summary string.
pub fn summary(&self) -> String {
format!(
"quads: {}, glyphs: {}, diagnostic_quads: {}, shadows: {}, paths: {}, underlines: {}, polychrome: {}, surfaces: {}",
self.quads.len(),
self.glyphs.len(),
self.diagnostic_quads.len(),
self.shadow_count,
self.path_count,
self.underline_count,
self.polychrome_sprite_count,
self.surface_count,
)
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub use test_scene::*;
#[derive(Default)]
pub(crate) struct Scene {
pub(crate) paint_operations: Vec<PaintOperation>,
@@ -32,6 +152,8 @@ pub(crate) struct Scene {
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
pub(crate) surfaces: Vec<PaintSurface>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) diagnostic_quads: Vec<test_scene::DiagnosticQuad>,
}
impl Scene {
@@ -46,6 +168,8 @@ impl Scene {
self.monochrome_sprites.clear();
self.polychrome_sprites.clear();
self.surfaces.clear();
#[cfg(any(test, feature = "test-support"))]
self.diagnostic_quads.clear();
}
pub fn len(&self) -> usize {
@@ -124,6 +248,41 @@ impl Scene {
}
}
/// Create a snapshot of the scene for testing.
#[cfg(any(test, feature = "test-support"))]
pub fn snapshot(&self) -> SceneSnapshot {
let quads = self
.quads
.iter()
.map(|q| RenderedQuad {
bounds: q.bounds,
background_color: q.background.as_solid(),
border_color: q.border_color,
})
.collect();
let glyphs = self
.monochrome_sprites
.iter()
.map(|s| RenderedGlyph {
origin: s.bounds.origin,
size: s.bounds.size,
color: s.color,
})
.collect();
SceneSnapshot {
quads,
glyphs,
diagnostic_quads: self.diagnostic_quads.clone(),
shadow_count: self.shadows.len(),
path_count: self.paths.len(),
underline_count: self.underlines.len(),
polychrome_sprite_count: self.polychrome_sprites.len(),
surface_count: self.surfaces.len(),
}
}
pub fn finish(&mut self) {
self.shadows.sort_by_key(|shadow| shadow.order);
self.quads.sort_by_key(|quad| quad.order);
@@ -134,6 +293,10 @@ impl Scene {
self.polychrome_sprites
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
self.surfaces.sort_by_key(|surface| surface.order);
#[cfg(any(test, feature = "test-support"))]
self.diagnostic_quads
.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
}
#[cfg_attr(
@@ -620,7 +783,7 @@ impl Default for TransformationMatrix {
#[repr(C)]
pub(crate) struct MonochromeSprite {
pub order: DrawOrder,
pub pad: u32, // align to 8 bytes
pub pad: u32,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
@@ -638,7 +801,7 @@ impl From<MonochromeSprite> for Primitive {
#[repr(C)]
pub(crate) struct PolychromeSprite {
pub order: DrawOrder,
pub pad: u32, // align to 8 bytes
pub pad: u32,
pub grayscale: bool,
pub opacity: f32,
pub bounds: Bounds<ScaledPixels>,

View File

@@ -44,6 +44,14 @@ impl ShapedLine {
self.layout.len
}
/// The width of the shaped line in pixels.
///
/// This is the glyph advance width computed by the text shaping system and is useful for
/// incrementally advancing a "pen" when painting multiple fragments on the same row.
pub fn width(&self) -> Pixels {
self.layout.width
}
/// Override the len, useful if you're rendering text a
/// as text b (e.g. rendering invisibles).
pub fn with_len(mut self, len: usize) -> Self {

View File

@@ -506,6 +506,10 @@ impl HitboxId {
///
/// See [`Hitbox::is_hovered`] for details.
pub fn is_hovered(self, window: &Window) -> bool {
// If this hitbox has captured the pointer, it's always considered hovered
if window.captured_hitbox == Some(self) {
return true;
}
let hit_test = &window.mouse_hit_test;
for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) {
if self == *id {
@@ -760,6 +764,11 @@ impl Frame {
self.tab_stops.clear();
self.focus = None;
#[cfg(any(test, feature = "test-support"))]
{
self.debug_bounds.clear();
}
#[cfg(any(feature = "inspector", debug_assertions))]
{
self.next_inspector_instance_ids.clear();
@@ -887,6 +896,9 @@ pub struct Window {
pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>,
prompt: Option<RenderablePromptHandle>,
pub(crate) client_inset: Option<Pixels>,
/// The hitbox that has captured the pointer, if any.
/// While captured, mouse events route to this hitbox regardless of hit testing.
captured_hitbox: Option<HitboxId>,
#[cfg(any(feature = "inspector", debug_assertions))]
inspector: Option<Entity<Inspector>>,
}
@@ -1311,6 +1323,7 @@ impl Window {
prompt: None,
client_inset: None,
image_cache_stack: Vec::new(),
captured_hitbox: None,
#[cfg(any(feature = "inspector", debug_assertions))]
inspector: None,
})
@@ -1994,6 +2007,26 @@ impl Window {
self.mouse_position
}
/// Captures the pointer for the given hitbox. While captured, all mouse move and mouse up
/// events will be routed to listeners that check this hitbox's `is_hovered` status,
/// regardless of actual hit testing. This enables drag operations that continue
/// even when the pointer moves outside the element's bounds.
///
/// The capture is automatically released on mouse up.
pub fn capture_pointer(&mut self, hitbox_id: HitboxId) {
self.captured_hitbox = Some(hitbox_id);
}
/// Releases any active pointer capture.
pub fn release_pointer(&mut self) {
self.captured_hitbox = None;
}
/// Returns the hitbox that has captured the pointer, if any.
pub fn captured_hitbox(&self) -> Option<HitboxId> {
self.captured_hitbox
}
/// The current state of the keyboard's modifiers
pub fn modifiers(&self) -> Modifiers {
self.modifiers
@@ -2966,6 +2999,41 @@ impl Window {
});
}
#[cfg(any(test, feature = "test-support"))]
/// Record a named diagnostic quad for test/debug snapshots.
///
/// This is intended for debugging and asserting against imperative painting logic. The
/// recorded quad does not affect rendering; it is captured alongside the rendered scene and
/// exposed via `scene_snapshot()`.
pub fn record_diagnostic_quad(
&mut self,
name: impl Into<SharedString>,
bounds: Bounds<Pixels>,
color: Option<Hsla>,
) {
self.invalidator.debug_assert_paint();
let scale_factor = self.scale_factor();
self.next_frame.scene.diagnostic_quads.push(crate::test_scene::DiagnosticQuad {
name: name.into(),
bounds: bounds.scale(scale_factor),
color,
});
}
#[cfg(not(any(test, feature = "test-support")))]
#[inline]
/// Record a named diagnostic quad for test/debug snapshots.
///
/// This is a no-op unless tests or the `test-support` feature are enabled.
pub fn record_diagnostic_quad(
&mut self,
_name: impl Into<SharedString>,
_bounds: Bounds<Pixels>,
_color: Option<Hsla>,
) {
}
/// Paint the given `Path` into the scene for the next frame at the current z-index.
///
/// This method should only be called as part of the paint phase of element drawing.
@@ -3850,6 +3918,11 @@ impl Window {
self.refresh();
}
}
// Auto-release pointer capture on mouse up
if event.is::<MouseUpEvent>() && self.captured_hitbox.is_some() {
self.captured_hitbox = None;
}
}
fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) {
@@ -4966,7 +5039,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
}
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct AnyWindowHandle {
pub(crate) id: WindowId,
state_type: TypeId,

View File

@@ -1801,9 +1801,7 @@ impl Buffer {
self.syntax_map.lock().did_parse(syntax_snapshot);
self.request_autoindent(cx);
self.parse_status.0.send(ParseStatus::Idle).unwrap();
if self.text.version() != *self.tree_sitter_data.version() {
self.invalidate_tree_sitter_data(self.text.snapshot());
}
self.invalidate_tree_sitter_data(self.text.snapshot());
cx.emit(BufferEvent::Reparsed);
cx.notify();
}

View File

@@ -295,6 +295,23 @@ impl LspInstaller for TyLspAdapter {
})
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let Some(ty_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await else {
return None;
};
let env = delegate.shell_env().await;
Some(LanguageServerBinary {
path: ty_bin,
env: Some(env),
arguments: vec!["server".into()],
})
}
async fn fetch_server_binary(
&self,
latest_version: Self::BinaryVersion,

View File

@@ -355,7 +355,7 @@ impl LspAdapter for RustLspAdapter {
| lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
) = completion.text_edit.as_ref()
&& let Ok(mut snippet) = snippet::Snippet::parse(new_text)
&& !snippet.tabstops.is_empty()
&& snippet.tabstops.len() > 1
{
label = String::new();
@@ -421,7 +421,9 @@ impl LspAdapter for RustLspAdapter {
0..label.rfind('(').unwrap_or(completion.label.len()),
highlight_id,
));
} else if detail_left.is_none() {
} else if detail_left.is_none()
&& kind != Some(lsp::CompletionItemKind::SNIPPET)
{
return None;
}
}
@@ -1597,6 +1599,40 @@ mod tests {
))
);
// Postfix completion without actual tabstops (only implicit final $0)
// The label should use completion.label so it can be filtered by "ref"
let ref_completion = adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::SNIPPET),
label: "ref".to_string(),
filter_text: Some("ref".to_string()),
label_details: Some(CompletionItemLabelDetails {
detail: None,
description: Some("&expr".to_string()),
}),
detail: Some("&expr".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::default(),
new_text: "&String::new()".to_string(),
})),
..Default::default()
},
&language,
)
.await;
assert!(
ref_completion.is_some(),
"ref postfix completion should have a label"
);
let ref_label = ref_completion.unwrap();
let filter_text = &ref_label.text[ref_label.filter_range.clone()];
assert!(
filter_text.contains("ref"),
"filter range text '{filter_text}' should contain 'ref' for filtering to work",
);
// Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
let res = adapter
.label_for_completion(

View File

@@ -155,15 +155,15 @@ impl Model {
pub fn max_token_count(&self) -> u64 {
match self {
Self::CodestralLatest => 256000,
Self::MistralLargeLatest => 131000,
Self::MistralLargeLatest => 256000,
Self::MistralMediumLatest => 128000,
Self::MistralSmallLatest => 32000,
Self::MagistralMediumLatest => 40000,
Self::MagistralSmallLatest => 40000,
Self::MagistralMediumLatest => 128000,
Self::MagistralSmallLatest => 128000,
Self::OpenMistralNemo => 131000,
Self::OpenCodestralMamba => 256000,
Self::DevstralMediumLatest => 128000,
Self::DevstralSmallLatest => 262144,
Self::DevstralMediumLatest => 256000,
Self::DevstralSmallLatest => 256000,
Self::Pixtral12BLatest => 128000,
Self::PixtralLargeLatest => 128000,
Self::Custom { max_tokens, .. } => *max_tokens,

View File

@@ -2610,9 +2610,8 @@ impl MultiBuffer {
for range in ranges {
let range = range.to_point(&snapshot);
let start = snapshot.point_to_offset(Point::new(range.start.row, 0));
let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0));
let start = start.saturating_sub_usize(1);
let end = snapshot.len().min(end + 1usize);
let end = (snapshot.point_to_offset(Point::new(range.end.row + 1, 0)) + 1usize)
.min(snapshot.len());
cursor.seek(&start, Bias::Right);
while let Some(item) = cursor.item() {
if *cursor.start() >= end {

View File

@@ -460,7 +460,7 @@ impl AgentServerStore {
.gemini
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(false),
.unwrap_or(true),
}),
);
self.external_agents.insert(

View File

@@ -1672,59 +1672,6 @@ impl GitStore {
}
}
fn mark_entries_pending_by_project_paths(
&mut self,
project_paths: &[ProjectPath],
stage: bool,
cx: &mut Context<Self>,
) {
let buffer_store = &self.buffer_store;
for project_path in project_paths {
let Some(buffer) = buffer_store.read(cx).get_by_path(project_path) else {
continue;
};
let buffer_id = buffer.read(cx).remote_id();
let Some(diff_state) = self.diffs.get(&buffer_id) else {
continue;
};
diff_state.update(cx, |diff_state, cx| {
let Some(uncommitted_diff) = diff_state.uncommitted_diff() else {
return;
};
let buffer_snapshot = buffer.read(cx).text_snapshot();
let file_exists = buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state().exists());
let all_hunks: Vec<_> = uncommitted_diff
.read(cx)
.hunks_intersecting_range(
text::Anchor::MIN..text::Anchor::MAX,
&buffer_snapshot,
cx,
)
.collect();
if !all_hunks.is_empty() {
uncommitted_diff.update(cx, |diff, cx| {
diff.stage_or_unstage_hunks(
stage,
&all_hunks,
&buffer_snapshot,
file_exists,
cx,
);
});
}
});
}
}
pub fn git_clone(
&self,
repo: String,
@@ -4253,28 +4200,6 @@ impl Repository {
save_futures
}
fn mark_entries_pending_for_stage(
&self,
entries: &[RepoPath],
stage: bool,
cx: &mut Context<Self>,
) {
let Some(git_store) = self.git_store() else {
return;
};
let mut project_paths = Vec::new();
for repo_path in entries {
if let Some(project_path) = self.repo_path_to_project_path(repo_path, cx) {
project_paths.push(project_path);
}
}
git_store.update(cx, move |git_store, cx| {
git_store.mark_entries_pending_by_project_paths(&project_paths, stage, cx);
});
}
pub fn stage_entries(
&mut self,
entries: Vec<RepoPath>,
@@ -4283,9 +4208,6 @@ impl Repository {
if entries.is_empty() {
return Task::ready(Ok(()));
}
self.mark_entries_pending_for_stage(&entries, true, cx);
let id = self.id;
let save_tasks = self.save_buffers(&entries, cx);
let paths = entries
@@ -4351,9 +4273,6 @@ impl Repository {
if entries.is_empty() {
return Task::ready(Ok(()));
}
self.mark_entries_pending_for_stage(&entries, false, cx);
let id = self.id;
let save_tasks = self.save_buffers(&entries, cx);
let paths = entries

View File

@@ -790,8 +790,7 @@ impl TerminalPanel {
}
pane.update(cx, |pane, cx| {
let focus = pane.has_focus(window, cx)
|| matches!(reveal_strategy, RevealStrategy::Always);
let focus = matches!(reveal_strategy, RevealStrategy::Always);
pane.add_item(terminal_view, true, focus, None, window, cx);
});
@@ -853,8 +852,7 @@ impl TerminalPanel {
}
pane.update(cx, |pane, cx| {
let focus = pane.has_focus(window, cx)
|| matches!(reveal_strategy, RevealStrategy::Always);
let focus = matches!(reveal_strategy, RevealStrategy::Always);
pane.add_item(terminal_view, true, focus, None, window, cx);
});
@@ -1171,64 +1169,67 @@ pub fn new_terminal_pane(
let source = tab.pane.clone();
let item_id_to_move = item.item_id();
let Ok(new_split_pane) = pane
.drag_split_direction()
.map(|split_direction| {
drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
let is_zoomed = if terminal_panel.active_pane == this_pane {
pane.is_zoomed()
} else {
terminal_panel.active_pane.read(cx).is_zoomed()
};
let new_pane = new_terminal_pane(
workspace.clone(),
project.clone(),
is_zoomed,
window,
cx,
);
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
terminal_panel.center.split(
&this_pane,
&new_pane,
split_direction,
cx,
)?;
anyhow::Ok(new_pane)
})
})
.transpose()
else {
return ControlFlow::Break(());
// If no split direction, let the regular pane drop handler take care of it
let Some(split_direction) = pane.drag_split_direction() else {
return ControlFlow::Continue(());
};
match new_split_pane.transpose() {
// Source pane may be the one currently updated, so defer the move.
Ok(Some(new_pane)) => cx
.spawn_in(window, async move |_, cx| {
cx.update(|window, cx| {
move_item(
&source,
&new_pane,
item_id_to_move,
new_pane.read(cx).active_item_index(),
true,
window,
cx,
// Gather data synchronously before deferring
let is_zoomed = drop_closure_terminal_panel
.upgrade()
.map(|terminal_panel| {
let terminal_panel = terminal_panel.read(cx);
if terminal_panel.active_pane == this_pane {
pane.is_zoomed()
} else {
terminal_panel.active_pane.read(cx).is_zoomed()
}
})
.unwrap_or(false);
let workspace = workspace.clone();
let terminal_panel = drop_closure_terminal_panel.clone();
// Defer the split operation to avoid re-entrancy panic.
// The pane may be the one currently being updated, so we cannot
// call mark_positions (via split) synchronously.
cx.spawn_in(window, async move |_, cx| {
cx.update(|window, cx| {
let Ok(new_pane) =
terminal_panel.update(cx, |terminal_panel, cx| {
let new_pane = new_terminal_pane(
workspace, project, is_zoomed, window, cx,
);
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
terminal_panel.center.split(
&this_pane,
&new_pane,
split_direction,
cx,
)?;
anyhow::Ok(new_pane)
})
.ok();
})
.detach(),
// If we drop into existing pane or current pane,
// regular pane drop handler will take care of it,
// using the right tab index for the operation.
Ok(None) => return ControlFlow::Continue(()),
err @ Err(_) => {
err.log_err();
return ControlFlow::Break(());
}
};
else {
return;
};
let Some(new_pane) = new_pane.log_err() else {
return;
};
move_item(
&source,
&new_pane,
item_id_to_move,
new_pane.read(cx).active_item_index(),
true,
window,
cx,
);
})
.ok();
})
.detach();
} else if let Some(project_path) = item.project_path(cx)
&& let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
{

View File

@@ -121,7 +121,7 @@ impl RenderOnce for Callout {
Severity::Info => (
IconName::Info,
Color::Muted,
cx.theme().colors().panel_background.opacity(0.),
cx.theme().status().info_background.opacity(0.1),
),
Severity::Success => (
IconName::Check,

View File

@@ -193,6 +193,12 @@ impl Render for ModalLayer {
background.fade_out(0.2);
this.bg(background)
})
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
this.hide_modal(window, cx);
}),
)
.child(
v_flex()
.h(px(0.0))

View File

@@ -3296,4 +3296,53 @@ mod tests {
assert_eq!(workspace.center_group, new_workspace.center_group);
}
#[gpui::test]
async fn test_empty_workspace_window_bounds() {
zlog::init_test();
let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
let id = db.next_id().await.unwrap();
// Create a workspace with empty paths (empty workspace)
let empty_paths: &[&str] = &[];
let display_uuid = Uuid::new_v4();
let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
origin: point(px(100.0), px(200.0)),
size: size(px(800.0), px(600.0)),
}));
let workspace = SerializedWorkspace {
id,
paths: PathList::new(empty_paths),
location: SerializedWorkspaceLocation::Local,
center_group: Default::default(),
window_bounds: None,
display: None,
docks: Default::default(),
breakpoints: Default::default(),
centered_layout: false,
session_id: None,
window_id: None,
user_toolchains: Default::default(),
};
// Save the workspace (this creates the record with empty paths)
db.save_workspace(workspace.clone()).await;
// Save window bounds separately (as the actual code does via set_window_open_status)
db.set_window_open_status(id, window_bounds, display_uuid)
.await
.unwrap();
// Retrieve it using empty paths
let retrieved = db.workspace_for_roots(empty_paths).unwrap();
// Verify window bounds were persisted
assert_eq!(retrieved.id, id);
assert!(retrieved.window_bounds.is_some());
assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
assert!(retrieved.display.is_some());
assert_eq!(retrieved.display.unwrap(), display_uuid);
}
}

View File

@@ -1748,26 +1748,18 @@ impl Workspace {
window
} else {
let window_bounds_override = window_bounds_env_override();
let is_empty_workspace = project_paths.is_empty();
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(WindowBounds::Windowed(bounds)), None)
} else if let Some(workspace) = serialized_workspace.as_ref() {
} else if let Some(workspace) = serialized_workspace.as_ref()
&& let Some(display) = workspace.display
&& let Some(bounds) = workspace.window_bounds.as_ref()
{
// Reopening an existing workspace - restore its saved bounds
if let (Some(display), Some(bounds)) =
(workspace.display, workspace.window_bounds.as_ref())
{
(Some(bounds.0), Some(display))
} else {
(None, None)
}
} else if is_empty_workspace {
// Empty workspace - try to restore the last known no-project window bounds
if let Some((display, bounds)) = persistence::read_default_window_bounds() {
(Some(bounds), Some(display))
} else {
(None, None)
}
(Some(bounds.0), Some(display))
} else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
// New or empty workspace - use the last known window bounds
(Some(bounds), Some(display))
} else {
// New window - let GPUI's default_bounds() handle cascading
(None, None)
@@ -5673,12 +5665,24 @@ impl Workspace {
persistence::DB.save_workspace(serialized_workspace).await;
})
}
WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| {
persistence::DB
.set_session_id(database_id, None)
.await
.log_err();
}),
WorkspaceLocation::DetachFromSession => {
let window_bounds = SerializedWindowBounds(window.window_bounds());
let display = window.display(cx).and_then(|d| d.uuid().ok());
window.spawn(cx, async move |_| {
persistence::DB
.set_window_open_status(
database_id,
window_bounds,
display.unwrap_or_default(),
)
.await
.log_err();
persistence::DB
.set_session_id(database_id, None)
.await
.log_err();
})
}
WorkspaceLocation::None => Task::ready(()),
}
}

View File

@@ -15,13 +15,11 @@ use extension::ExtensionHostProxy;
use fs::{Fs, RealFs};
use futures::{StreamExt, channel::oneshot, future};
use git::GitHostingProviderRegistry;
use git_ui::clone::clone_and_open;
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _};
use gpui_tokio::Tokio;
use language::LanguageRegistry;
use onboarding::{FIRST_OPEN, show_onboarding_view};
use project_panel::ProjectPanel;
use prompt_store::PromptBuilder;
use remote::RemoteConnectionOptions;
use reqwest_client::ReqwestClient;
@@ -35,12 +33,10 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use session::{AppSession, Session};
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
use std::{
cell::RefCell,
env,
io::{self, IsTerminal},
path::{Path, PathBuf},
process,
rc::Rc,
sync::{Arc, OnceLock},
time::Instant,
};
@@ -897,41 +893,6 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
})
.detach_and_log_err(cx);
}
OpenRequestKind::GitClone { repo_url } => {
workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
if window.is_window_active() {
clone_and_open(
repo_url,
cx.weak_entity(),
window,
cx,
Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
workspace.focus_panel::<ProjectPanel>(window, cx);
}),
);
return;
}
let subscription = Rc::new(RefCell::new(None));
subscription.replace(Some(cx.observe_in(&cx.entity(), window, {
let subscription = subscription.clone();
let repo_url = repo_url.clone();
move |_, workspace_entity, window, cx| {
if window.is_window_active() && subscription.take().is_some() {
clone_and_open(
repo_url.clone(),
workspace_entity.downgrade(),
window,
cx,
Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
workspace.focus_panel::<ProjectPanel>(window, cx);
}),
);
}
}
})));
});
}
OpenRequestKind::GitCommit { sha } => {
cx.spawn(async move |cx| {
let paths_with_position =

View File

@@ -25,7 +25,6 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use ui::SharedString;
use util::ResultExt;
use util::paths::PathWithPosition;
use workspace::PathList;
@@ -59,9 +58,6 @@ pub enum OpenRequestKind {
/// `None` opens settings without navigating to a specific path.
setting_path: Option<String>,
},
GitClone {
repo_url: SharedString,
},
GitCommit {
sha: String,
},
@@ -117,8 +113,6 @@ impl OpenRequest {
this.kind = Some(OpenRequestKind::Setting {
setting_path: Some(setting_path.to_string()),
});
} else if let Some(clone_path) = url.strip_prefix("zed://git/clone") {
this.parse_git_clone_url(clone_path)?
} else if let Some(commit_path) = url.strip_prefix("zed://git/commit/") {
this.parse_git_commit_url(commit_path)?
} else if url.starts_with("ssh://") {
@@ -149,26 +143,6 @@ impl OpenRequest {
}
}
fn parse_git_clone_url(&mut self, clone_path: &str) -> Result<()> {
// Format: /?repo=<url> or ?repo=<url>
let clone_path = clone_path.strip_prefix('/').unwrap_or(clone_path);
let query = clone_path
.strip_prefix('?')
.context("invalid git clone url: missing query string")?;
let repo_url = url::form_urlencoded::parse(query.as_bytes())
.find_map(|(key, value)| (key == "repo").then_some(value))
.filter(|s| !s.is_empty())
.context("invalid git clone url: missing repo query parameter")?
.to_string()
.into();
self.kind = Some(OpenRequestKind::GitClone { repo_url });
Ok(())
}
fn parse_git_commit_url(&mut self, commit_path: &str) -> Result<()> {
// Format: <sha>?repo=<path>
let (sha, query) = commit_path
@@ -1113,80 +1087,4 @@ mod tests {
assert!(!errored_reuse);
}
#[gpui::test]
fn test_parse_git_clone_url(cx: &mut TestAppContext) {
let _app_state = init_test(cx);
let request = cx.update(|cx| {
OpenRequest::parse(
RawOpenRequest {
urls: vec![
"zed://git/clone/?repo=https://github.com/zed-industries/zed.git".into(),
],
..Default::default()
},
cx,
)
.unwrap()
});
match request.kind {
Some(OpenRequestKind::GitClone { repo_url }) => {
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
}
_ => panic!("Expected GitClone kind"),
}
}
#[gpui::test]
fn test_parse_git_clone_url_without_slash(cx: &mut TestAppContext) {
let _app_state = init_test(cx);
let request = cx.update(|cx| {
OpenRequest::parse(
RawOpenRequest {
urls: vec![
"zed://git/clone?repo=https://github.com/zed-industries/zed.git".into(),
],
..Default::default()
},
cx,
)
.unwrap()
});
match request.kind {
Some(OpenRequestKind::GitClone { repo_url }) => {
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
}
_ => panic!("Expected GitClone kind"),
}
}
#[gpui::test]
fn test_parse_git_clone_url_with_encoding(cx: &mut TestAppContext) {
let _app_state = init_test(cx);
let request = cx.update(|cx| {
OpenRequest::parse(
RawOpenRequest {
urls: vec![
"zed://git/clone/?repo=https%3A%2F%2Fgithub.com%2Fzed-industries%2Fzed.git"
.into(),
],
..Default::default()
},
cx,
)
.unwrap()
});
match request.kind {
Some(OpenRequestKind::GitClone { repo_url }) => {
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
}
_ => panic!("Expected GitClone kind"),
}
}
}

View File

@@ -354,6 +354,8 @@ pub mod agent {
ResetAgentZoom,
/// Toggles the utility/agent pane open/closed state.
ToggleAgentPane,
/// Pastes clipboard content without any formatting.
PasteRaw,
]
);
}

View File

@@ -15,6 +15,10 @@ When there is an appropriate language server available, Zed will provide complet
You can manually trigger completions with `ctrl-space` or by triggering the `editor::ShowCompletions` action from the command palette.
> Note: Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
> Open **System Settings** > **Keyboard** > **Keyboard Shortcut**s >
> **Input Sources** and uncheck **Select the previous input source**.
For more information, see:
- [Configuring Supported Languages](./configuring-languages.md)

View File

@@ -45,11 +45,15 @@ pub(crate) fn run_tests() -> Workflow {
&should_run_tests,
]);
let check_style = check_style();
let run_tests_linux = run_platform_tests(Platform::Linux);
let call_autofix = call_autofix(&check_style, &run_tests_linux);
let mut jobs = vec![
orchestrate,
check_style(),
check_style,
should_run_tests.guard(run_platform_tests(Platform::Windows)),
should_run_tests.guard(run_platform_tests(Platform::Linux)),
should_run_tests.guard(run_tests_linux),
should_run_tests.guard(run_platform_tests(Platform::Mac)),
should_run_tests.guard(doctests()),
should_run_tests.guard(check_workspace_binaries()),
@@ -106,6 +110,7 @@ pub(crate) fn run_tests() -> Workflow {
workflow
})
.add_job(tests_pass.name, tests_pass.job)
.add_job(call_autofix.name, call_autofix.job)
}
// Generates a bash script that checks changed files against regex patterns
@@ -221,6 +226,8 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob {
named::job(job)
}
pub const STYLE_FAILED_OUTPUT: &str = "style_failed";
fn check_style() -> NamedJob {
fn check_for_typos() -> Step<Use> {
named::uses(
@@ -236,15 +243,58 @@ fn check_style() -> NamedJob {
.add_step(steps::checkout_repo())
.add_step(steps::cache_rust_dependencies_namespace())
.add_step(steps::setup_pnpm())
.add_step(steps::script("./script/prettier"))
.add_step(steps::prettier())
.add_step(steps::cargo_fmt())
.add_step(steps::trigger_autofix(false))
.add_step(steps::record_style_failure())
.add_step(steps::script("./script/check-todos"))
.add_step(steps::script("./script/check-keymaps"))
.add_step(check_for_typos()),
.add_step(check_for_typos())
.outputs([(
STYLE_FAILED_OUTPUT.to_owned(),
format!(
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
steps::RECORD_STYLE_FAILURE_STEP_ID
),
)]),
)
}
fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob {
fn dispatch_autofix(run_tests_linux_name: &str) -> Step<Run> {
let clippy_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
run_tests_linux_name, CLIPPY_FAILED_OUTPUT
);
named::bash(format!(
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}",
clippy_failed_expr
))
.add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}"))
}
let style_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
check_style.name, STYLE_FAILED_OUTPUT
);
let clippy_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
run_tests_linux.name, CLIPPY_FAILED_OUTPUT
);
let (authenticate, _token) = steps::authenticate_as_zippy();
let job = Job::default()
.runs_on(runners::LINUX_SMALL)
.cond(Expression::new(format!(
"always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
style_failed_expr, clippy_failed_expr
)))
.needs(vec![check_style.name.clone(), run_tests_linux.name.clone()])
.add_step(authenticate)
.add_step(dispatch_autofix(&run_tests_linux.name));
named::job(job)
}
fn check_dependencies() -> NamedJob {
fn install_cargo_machete() -> Step<Use> {
named::uses(
@@ -305,6 +355,8 @@ fn check_workspace_binaries() -> NamedJob {
)
}
pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed";
pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
let runner = match platform {
Platform::Windows => runners::WINDOWS_DEFAULT,
@@ -327,12 +379,23 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
.add_step(steps::setup_node())
.add_step(steps::clippy(platform))
.when(platform == Platform::Linux, |job| {
job.add_step(steps::trigger_autofix(true))
.add_step(steps::cargo_install_nextest())
job.add_step(steps::record_clippy_failure())
})
.when(platform == Platform::Linux, |job| {
job.add_step(steps::cargo_install_nextest())
})
.add_step(steps::clear_target_dir_if_large(platform))
.add_step(steps::cargo_nextest(platform))
.add_step(steps::cleanup_cargo_config(platform)),
.add_step(steps::cleanup_cargo_config(platform))
.when(platform == Platform::Linux, |job| {
job.outputs([(
CLIPPY_FAILED_OUTPUT.to_owned(),
format!(
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
steps::RECORD_CLIPPY_FAILURE_STEP_ID
),
)])
}),
}
}

View File

@@ -54,8 +54,25 @@ pub fn setup_sentry() -> Step<Use> {
.add_with(("token", vars::SENTRY_AUTH_TOKEN))
}
pub const PRETTIER_STEP_ID: &str = "prettier";
pub const CARGO_FMT_STEP_ID: &str = "cargo_fmt";
pub const RECORD_STYLE_FAILURE_STEP_ID: &str = "record_style_failure";
pub fn prettier() -> Step<Run> {
named::bash("./script/prettier").id(PRETTIER_STEP_ID)
}
pub fn cargo_fmt() -> Step<Run> {
named::bash("cargo fmt --all -- --check")
named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID)
}
pub fn record_style_failure() -> Step<Run> {
named::bash(format!(
"echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
PRETTIER_STEP_ID, CARGO_FMT_STEP_ID
))
.id(RECORD_STYLE_FAILURE_STEP_ID)
.if_condition(Expression::new("always()"))
}
pub fn cargo_install_nextest() -> Step<Use> {
@@ -101,13 +118,25 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
}
}
pub const CLIPPY_STEP_ID: &str = "clippy";
pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure";
pub fn clippy(platform: Platform) -> Step<Run> {
match platform {
Platform::Windows => named::pwsh("./script/clippy.ps1"),
_ => named::bash("./script/clippy"),
Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID),
_ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
}
}
pub fn record_clippy_failure() -> Step<Run> {
named::bash(format!(
"echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
CLIPPY_STEP_ID
))
.id(RECORD_CLIPPY_FAILURE_STEP_ID)
.if_condition(Expression::new("always()"))
}
pub fn cache_rust_dependencies_namespace() -> Step<Use> {
named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
}
@@ -345,16 +374,6 @@ pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
))
}
pub fn trigger_autofix(run_clippy: bool) -> Step<Run> {
named::bash(format!(
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy={run_clippy}"
))
.if_condition(Expression::new(
"failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
))
.add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
}
pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
let step = named::uses(
"actions",