Compare commits

...

77 Commits

Author SHA1 Message Date
Junkui Zhang
8276554222 fix merge conflicts 2025-07-10 16:28:10 +08:00
Junkui Zhang
2400ed34e0 fix merge conflicts 2025-07-10 14:06:42 +08:00
Junkui Zhang
ce23637dc1 fix test 2025-07-10 13:46:56 +08:00
Junkui Zhang
7bcd6d839f use Keymap 2025-07-10 13:46:56 +08:00
Junkui Zhang
20b9989b79 remove unused 2025-07-10 13:46:56 +08:00
Junkui Zhang
3269029a3e clippy 2025-07-10 13:46:56 +08:00
Junkui Zhang
ea0e908714 fix merge conflicts 2025-07-10 13:46:56 +08:00
Junkui Zhang
8b69ccb488 actually fix x11 2025-07-10 13:46:24 +08:00
Junkui Zhang
2d6d53219b try fix x11 2025-07-10 13:46:24 +08:00
Junkui Zhang
de615870cc fix ci 2025-07-10 13:46:24 +08:00
Junkui Zhang
f1f9470a14 simplify 2025-07-10 13:46:24 +08:00
Junkui Zhang
9433c381eb remove debug print 2025-07-10 13:46:24 +08:00
Junkui Zhang
6ede6a1573 test 2025-07-10 13:46:24 +08:00
Junkui Zhang
7e0d74db8a simplify 2025-07-10 13:46:24 +08:00
Junkui Zhang
e019ce8260 clippy 2025-07-10 13:46:24 +08:00
Junkui Zhang
7ab7eab54e fix headless 2025-07-10 13:46:24 +08:00
Junkui Zhang
7da2e2af50 use cache for x11 2025-07-10 13:46:24 +08:00
Junkui Zhang
92a986cffd use cache for wayland 2025-07-10 13:46:24 +08:00
Junkui Zhang
8b6658c650 refactor keyboard layout 2025-07-10 13:46:24 +08:00
Junkui Zhang
9bc4c0f7f5 try fix x11 2025-07-10 13:46:24 +08:00
Junkui Zhang
f829a178f6 fix group 2025-07-10 13:46:24 +08:00
Junkui Zhang
2df80de880 fix 2025-07-10 13:46:24 +08:00
Junkui Zhang
2976b99b14 update group 2025-07-10 13:46:24 +08:00
Junkui Zhang
ad3d44119a fix all 2025-07-10 13:46:24 +08:00
Junkui Zhang
0b5a264f88 revert Instroduce KeyboardState 2025-07-10 13:46:07 +08:00
Junkui Zhang
f22d06cf6e Revert "test"
This reverts commit 8e773a8fcdffbaa754098da3f93c6ede28bf75a3.
2025-07-10 13:46:00 +08:00
Junkui Zhang
26718da9bb test 2025-07-10 13:46:00 +08:00
Junkui Zhang
671a91e69d remvoe deps of state 2025-07-10 13:46:00 +08:00
Junkui Zhang
bbda5d4f78 test 2025-07-10 13:46:00 +08:00
Junkui Zhang
2fb674088f remove unused 2025-07-10 13:46:00 +08:00
Junkui Zhang
5c4ea49793 fix dvoka 2025-07-10 13:46:00 +08:00
Junkui Zhang
21e7cc3fed add letters 2025-07-10 13:46:00 +08:00
Junkui Zhang
f53168c56c test 2025-07-10 13:46:00 +08:00
Junkui Zhang
3182f14972 add shift test 2025-07-10 13:46:00 +08:00
Junkui Zhang
eca211e0f1 add helper 2025-07-10 13:46:00 +08:00
Junkui Zhang
d6add799dc try add a test 2025-07-10 13:46:00 +08:00
Junkui Zhang
e9649dc25c fix 2025-07-10 13:46:00 +08:00
Junkui Zhang
71daf47ad5 fix deadkey 2025-07-10 13:45:28 +08:00
Junkui Zhang
66c6f5066e fix russian layout 2025-07-10 13:45:28 +08:00
Junkui Zhang
be79ccde07 fix all 2025-07-10 13:45:28 +08:00
Junkui Zhang
061ba9b6d2 use it 2025-07-10 13:45:28 +08:00
Junkui Zhang
1ba3c2f589 fix wayland 2025-07-10 13:45:28 +08:00
Junkui Zhang
bbee877ca8 fix x11 2025-07-10 13:45:28 +08:00
Junkui Zhang
c6e697fc7f add KeyboardState 2025-07-10 13:44:12 +08:00
Junkui Zhang
b7b83105fc add mapper 2025-07-10 13:44:12 +08:00
Junkui Zhang
7d901c5e47 parse immutable 2025-07-10 13:44:12 +08:00
Junkui Zhang
1a8d1944b0 test 2025-07-10 13:44:12 +08:00
Junkui Zhang
35db644beb init 2025-07-10 13:44:12 +08:00
张小白
76e52ea374 ci: Improve windows_tests description (#34123)
We're running clippy and tests in our ci

Release Notes:

- N/A
2025-07-09 23:26:48 -04:00
Conrad Irwin
ca0f0cc8d1 vim: Fix panic when scrolling beyond last line (#34172)
cc @dinocosta

Release Notes:

- (preview only) vim: Fix panic when scrolling down at end of file
2025-07-10 01:37:16 +00:00
张小白
a133c1311d windows: Fix ctrl-r showing the control character (#34171)
Release Notes:

- N/A
2025-07-10 01:15:33 +00:00
Agus Zubiaga
08ffd9884a Update to acp 0.0.6 (#34159)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-09 19:14:29 -06:00
Danilo Leal
dc591fe7c7 Hide unused extension types from the extension UI (#34166)
This PR hides "slash commands" and "indexed docs providers" from the
extensions UI as these are virtually completely unused types of
extensions.

Release Notes:

- N/A
2025-07-09 23:57:35 +00:00
Danilo Leal
95784d53ca agent: Fix edit bar's background color when zoomed in (#34163)
When the panel was zoomed in, the edit bar's background color would have
a different color than the rest of the panel. This PR fixes it by using
the `panel_background` color token.

Release Notes:

- N/A
2025-07-09 23:25:56 +00:00
Ben Kunkle
9b63ba6205 gpui: Add cx.intercept_keystrokes API to intercept keystrokes before action dispatch (#34084)
Closes #ISSUE

`cx.intercept_keystrokes` functions as a sibling API to
`cx.observe_keystrokes`. Under the hood the two API's are basically
identical, however, `cx.observe_keystrokes` runs _after_ all event
dispatch handling (including action dispatch) while
`cx.intercept_keystrokes` runs _before_. This allows for
`cx.stop_propagation()` calls within the `cx.intercept_keystrokes`
callback to prevent action dispatch.

The motivating example usage behind this API is also included in this
PR. It is used as part of a keystroke input component that needs to
intercept keystrokes before action dispatch to display them.

cc: @mikayla-maki 

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-09 22:44:32 +00:00
Danilo Leal
862e733ef5 agent: Make all icons used for tool calls consistent (#34160)
Starting to use the `Tool...` family of icons dedicated & designed for
the agent panel.

Release Notes:

- N/A
2025-07-09 19:28:46 -03:00
Smit Barmase
66dda8e368 editor: Fix block comment with same prefix as line comment incorrectly extending on new line (#34156)
Closes #33930

Release Notes:

- Fixed `--[[` incorrectly extending `--` upon a new line in Lua.
2025-07-10 02:54:05 +05:30
Bennet Bo Fenner
16d02cfdb3 agent: Allow thinking in edit file tool (#34155)
Follow up to #34141. As pointed out by @maan2003 changing the thinking
parameters invalidates the message cache
([Docs](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#what-invalidates-the-cache)).

All the other places where `thinking_allowed ` is set to `false` should
be fine since we do not do any caching there.

Release Notes:

- N/A
2025-07-09 21:08:32 +00:00
Julia Ryan
2c41e10c98 Add button to view dap logs (#34153)
Release Notes:

- N/A
2025-07-09 20:51:45 +00:00
Danilo Leal
9ab5e78b79 Add some UI adjustments to the keymap editor (#34152)
Mostly simple things like spacing and colors.

Release Notes:

- N/A
2025-07-09 17:11:24 -03:00
Danilo Leal
e30e4381de Add chevron icons to the dropdown menus in the LSP log pane (#34149)
A bunch of dropdown menus in this pane that weren't clear that were
menus without chevrons in it. :)

<img
src="https://github.com/user-attachments/assets/fc701a5d-ed89-4de3-a76e-06d22ad9e366"
width="600"/>

Release Notes:

- N/A
2025-07-09 16:33:07 -03:00
Steve Hillier
de627ba04d git_ui: Support "Open Pull Request" for more platforms (#33833)
Hello! It would be great to be able to use the "Open Pull Request"
button that appears after pushing a branch via the git UI on more
platforms (I use Gitlab day to day). Would you be open to adding more
variations of the PR hint text?

I've added the text that Gitlab and Bitbucket use in their push logs
here.

Release Notes:

- Git UI: Support "Open Pull Request" for more platforms
2025-07-09 19:09:59 +00:00
Danilo Leal
80eed63255 agent: Add menu in the plus icon button for creating a new thread (#34143)
Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-07-09 15:44:45 -03:00
Danilo Leal
974bc4096a agent: Don't always render the markdown/scroll buttons (#34145)
Release Notes:

- N/A

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-07-09 15:44:39 -03:00
Max Brunsfeld
642d8bb8f5 Don't upload windows installer to preview releases for now (#34147)
Release Notes:

- N/A
2025-07-09 11:31:11 -07:00
Bennet Bo Fenner
41fe2a2ab4 agent: Disable thinking when using inline assistant/edit file tool (#34141)
This introduces a new field `thinking_allowed` on `LanguageModelRequest`
which lets us control whether thinking should be enabled if the model
supports it.
We permit thinking in the Inline Assistant, Edit File tool and the Git
Commit message generator, this should make generation faster when using
a thinking model, e.g. `claude-sonnet-4-thinking`

Release Notes:

- N/A
2025-07-09 18:05:39 +00:00
Richard Feldman
96ff6d86a3 Fix autocomplete on settings.json after reload (#34142)
Closes #31850


https://github.com/user-attachments/assets/6182ef64-3ce6-49ed-a91b-770c51cb6e94

Release Notes:

- Fixed autocomplete on settings.json after restarting Zed
2025-07-09 13:43:17 -04:00
localcc
6e5763215f Simplify installer by removing a couple pages (#34144)
Release Notes:

- N/A
2025-07-09 19:41:41 +02:00
localcc
de0e6f716c Fix inno dir (#34116)
Fix inno dir for nightly builds

Release Notes:

- N/A
2025-07-09 19:03:53 +02:00
Anthony Eid
93bfae71dc Show conflicts in the keymap editor (#34137)
This PR shows conflicts in a user's keymap editor by adding an error
background to a conflicting row and allows users to filter the keymap
editor by conflicts.

A key binding is determined to have a conflict if any other binding has
the same context and key strokes. In the future, this could be further
improved upon by normalizing bindings’ context so it's not just a string
comparison.


Release Notes:

- N/A

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
2025-07-09 16:56:33 +00:00
Ben Kunkle
171be7e009 keymap_ui: Render <no arguments> for bindings that take arguments where none are provided (#34140)
Closes #ISSUE

Adds a visual indicator to the `Arguments` column of the keymap table to
help distinguish between actions that don't take arguments, and actions
that take arguments but none were provided.

Currently, the `<no arguments>` indicator is rendered only in the latter
case, when no arguments are provided to an action that could take
arguments, as the inverse results in almost every row containing the
indicator which is quite noisy.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-09 16:48:35 +00:00
Smit Barmase
d5cc1cbaa9 title_bar: Add setting to always show menu for Linux and Windows (#34139)
Closes #22869

Release Notes:

- Added `show_menus` setting to always show menu bar for Linux and
Windows.

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-09 09:44:11 -07:00
Ben Kunkle
6d26f107dd keymap_ui: Creating keybinds (#34138)
Adds support for creating new keybinds in the keymap UI. "Create" means
two different things for existing bindings, and unbound actions.
- For existing bindings, it is essentially a duplicate + edit
- For unbound actions, it is the creation of a binding

Release Notes:

- N/A
2025-07-09 16:24:33 +00:00
Kristian Gosvig
e2b9dfa89c editor: Add CollapseAllDiffHunks docs (#34124)
## Summary

This PR improves the documentation for diff hunk keyboard shortcuts to
make existing functionality more discoverable to users.

### Problem

Users were unaware that pressing **ESC** already collapses all expanded
diff hunks through the existing `Cancel` action. The functionality
exists but lacks discoverability:

- The `Cancel` action description was too generic: "Cancels the current
operation"
- No documentation existed for diff hunk keyboard shortcuts in the Git
docs
- Users would naturally expect a dedicated keybinding for collapsing
diff hunks

Release Notes:

- N/A
2025-07-09 16:10:35 +00:00
Conrad Irwin
495ec7a109 ACP (#34030)
Implements an ACP client that can be used from the agent panel
2025-07-09 16:02:31 +00:00
Oleksiy Syvokon
b9b42bee99 evals: Fix bug that prevented multiple turns from displaying (#34128)
Release Notes:

- N/A
2025-07-09 15:31:58 +00:00
Peter Tripp
a9b82e1e57 Bump Zed to v0.196 (#34127)
Release Notes:

- N/A
2025-07-09 11:04:13 -04:00
111 changed files with 6448 additions and 672 deletions

View File

@@ -1,6 +1,12 @@
name: "Run tests"
description: "Runs the tests"
inputs:
use-xvfb:
description: "Whether to run tests with xvfb"
required: false
default: "false"
runs:
using: "composite"
steps:
@@ -20,4 +26,9 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast
run: |
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
else
cargo nextest run --workspace --no-fail-fast
fi

View File

@@ -334,6 +334,8 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
with:
use-xvfb: true
- name: Build other binaries and features
run: |
@@ -390,7 +392,7 @@ jobs:
windows_tests:
timeout-minutes: 60
name: (Windows) Run Tests
name: (Windows) Run Clippy and tests
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
@@ -800,7 +802,8 @@ jobs:
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
# Re-enable when we are ready to publish windows preview releases
if: false && ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}

70
Cargo.lock generated
View File

@@ -2,6 +2,33 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "acp"
version = "0.1.0"
dependencies = [
"agent_servers",
"agentic-coding-protocol",
"anyhow",
"async-pipe",
"buffer_diff",
"editor",
"env_logger 0.11.8",
"futures 0.3.31",
"gpui",
"indoc",
"itertools 0.14.0",
"language",
"markdown",
"project",
"serde_json",
"settings",
"smol",
"tempfile",
"ui",
"util",
"workspace-hack",
]
[[package]]
name = "activity_indicator"
version = "0.1.0"
@@ -107,6 +134,24 @@ dependencies = [
"zstd",
]
[[package]]
name = "agent_servers"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"futures 0.3.31",
"gpui",
"paths",
"project",
"schemars",
"serde",
"settings",
"util",
"which 6.0.3",
"workspace-hack",
]
[[package]]
name = "agent_settings"
version = "0.1.0"
@@ -130,8 +175,11 @@ dependencies = [
name = "agent_ui"
version = "0.1.0"
dependencies = [
"acp",
"agent",
"agent_servers",
"agent_settings",
"agentic-coding-protocol",
"anyhow",
"assistant_context",
"assistant_slash_command",
@@ -191,6 +239,7 @@ dependencies = [
"settings",
"smol",
"streaming_diff",
"task",
"telemetry",
"telemetry_events",
"terminal",
@@ -212,6 +261,22 @@ dependencies = [
"zed_llm_client",
]
[[package]]
name = "agentic-coding-protocol"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ac0351749af7bf53c65042ef69fefb9351aa8b7efa0a813d6281377605c37d"
dependencies = [
"anyhow",
"chrono",
"futures 0.3.31",
"log",
"parking_lot",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -14078,6 +14143,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
"chrono",
"dyn-clone",
"indexmap",
"ref-cast",
@@ -19579,6 +19645,7 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
"schemars",
"scopeguard",
"sea-orm",
"sea-query-binder",
@@ -19972,10 +20039,11 @@ dependencies = [
[[package]]
name = "zed"
version = "0.195.0"
version = "0.196.0"
dependencies = [
"activity_indicator",
"agent",
"agent_servers",
"agent_settings",
"agent_ui",
"anyhow",

View File

@@ -2,9 +2,11 @@
resolver = "2"
members = [
"crates/activity_indicator",
"crates/acp",
"crates/agent_ui",
"crates/agent",
"crates/agent_settings",
"crates/agent_servers",
"crates/anthropic",
"crates/askpass",
"crates/assets",
@@ -216,10 +218,12 @@ edition = "2024"
# Workspace member crates
#
activity_indicator = { path = "crates/activity_indicator" }
acp = { path = "crates/acp" }
agent = { path = "crates/agent" }
activity_indicator = { path = "crates/activity_indicator" }
agent_ui = { path = "crates/agent_ui" }
agent_settings = { path = "crates/agent_settings" }
agent_servers = { path = "crates/agent_servers" }
ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
@@ -400,6 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agentic-coding-protocol = "0.0.6"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>

Before

Width:  |  Height:  |  Size: 358 B

View File

@@ -1,5 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.66659 6.5L6.33325 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.33325 6.5L9.66659 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 804 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-code"><path d="m13 13.5 2-2.5-2-2.5"/><path d="m21 21-4.3-4.3"/><path d="M9 8.5 7 11l2 2.5"/><circle cx="11" cy="11" r="8"/></svg>

Before

Width:  |  Height:  |  Size: 340 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4174 10.2159C10.5454 9.58974 10.4174 9.57261 11.3762 8.46959C11.9337 7.82822 12.335 7.09214 12.335 6.27818C12.335 5.28184 11.9309 4.32631 11.2118 3.62179C10.4926 2.91728 9.5171 2.52148 8.50001 2.52148C7.48291 2.52148 6.50748 2.91728 5.78828 3.62179C5.06909 4.32631 4.66504 5.28184 4.66504 6.27818C4.66504 6.9043 4.79288 7.65565 5.62379 8.46959C6.58253 9.59098 6.45474 9.58974 6.58257 10.2159M10.4174 10.2159L10.4174 12.2989C10.4174 12.9504 9.87836 13.4786 9.21329 13.4786H7.78674C7.12167 13.4786 6.58253 12.9504 6.58253 12.2989L6.58257 10.2159M10.4174 10.2159H8.50001H6.58257" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 2.5H6.5C6.22386 2.5 6 2.83579 6 3.25V4.75C6 5.16421 6.22386 5.5 6.5 5.5H9.5C9.77614 5.5 10 5.16421 10 4.75V3.25C10 2.83579 9.77614 2.5 9.5 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 3.5H11C11.2652 3.5 11.5196 3.61706 11.7071 3.82544C11.8946 4.03381 12 4.31643 12 4.61111V12.3889C12 12.6836 11.8946 12.9662 11.7071 13.1746C11.5196 13.3829 11.2652 13.5 11 13.5H5C4.73478 13.5 4.48043 13.3829 4.29289 13.1746C4.10536 12.9662 4 12.6836 4 12.3889V4.61111C4 4.31643 4.10536 4.03381 4.29289 3.82544C4.48043 3.61706 4.73478 3.5 5 3.5H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 788 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.50002 2.5H5C4.73478 2.5 4.48043 2.6159 4.29289 2.82219C4.10535 3.02848 4 3.30826 4 3.6V12.3999C4 12.6917 4.10535 12.9715 4.29289 13.1778C4.48043 13.3841 4.73478 13.5 5 13.5H11C11.2652 13.5 11.5195 13.3841 11.7071 13.1778C11.8946 12.9715 12 12.6917 12 12.3999V5.25L9.50002 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.3427 6.82379L6.65698 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.65698 6.82379L9.3427 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 724 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7244 11.5299L9.01711 3.2922C8.91447 3.11109 8.76562 2.96045 8.58576 2.85564C8.4059 2.75084 8.20145 2.69562 7.99328 2.69562C7.7851 2.69562 7.58066 2.75084 7.40079 2.85564C7.22093 2.96045 7.07209 3.11109 6.96945 3.2922L2.26218 11.5299C2.15844 11.7096 2.10404 11.9135 2.1045 12.121C2.10495 12.3285 2.16026 12.5321 2.2648 12.7113C2.36934 12.8905 2.5194 13.0389 2.69978 13.1415C2.88015 13.244 3.08443 13.297 3.2919 13.2951H12.7064C12.9129 13.2949 13.1157 13.2404 13.2944 13.137C13.4731 13.0336 13.6215 12.8851 13.7247 12.7062C13.8278 12.5273 13.8821 12.3245 13.882 12.118C13.882 11.9115 13.8276 11.7087 13.7244 11.5299Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99927 6.23425V8.58788" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99927 10.9415H8.00492" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4 12.5C12.6917 12.5 12.9715 12.3884 13.1778 12.1899C13.3841 11.9913 13.5 11.722 13.5 11.4412V6.14706C13.5 5.86624 13.3841 5.59693 13.1778 5.39836C12.9715 5.19979 12.6917 5.08824 12.4 5.08824H8.055C7.87103 5.08997 7.68955 5.04726 7.52717 4.96402C7.36478 4.88078 7.22668 4.75967 7.1255 4.61176L6.68 3.97647C6.57984 3.83007 6.44349 3.7099 6.28317 3.62674C6.12286 3.54358 5.94361 3.50003 5.7615 3.5H3.6C3.30826 3.5 3.02847 3.61155 2.82218 3.81012C2.61589 4.00869 2.5 4.27801 2.5 4.55882V11.4412C2.5 11.722 2.61589 11.9913 2.82218 12.1899C3.02847 12.3884 3.30826 12.5 3.6 12.5H12.4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 778 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 8.5L4.94864 12.6222C4.71647 12.8544 4.40157 12.9848 4.07323 12.9848C3.74488 12.9848 3.42999 12.8544 3.19781 12.6222C2.96564 12.39 2.83521 12.0751 2.83521 11.7468C2.83521 11.4185 2.96564 11.1036 3.19781 10.8714L7.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.8352 9.98474L13.8352 6.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.8352 7.42495L11.7634 6.4298C11.5533 6.23484 11.4353 5.97039 11.4352 5.69462V5.08526L10.1696 3.91022C9.54495 3.33059 8.69961 3.00261 7.81649 2.99722L5.83521 2.98474L6.35041 3.41108C6.71634 3.71233 7.00935 4.08216 7.21013 4.4962C7.4109 4.91024 7.51488 5.35909 7.51521 5.81316L7.5 6.5L9 8.5L9.5 8C9.5 8 9.87337 7.79457 10.0834 7.98959L11.1552 8.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 988 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 12C6.65203 12.304 6.87068 12.5565 7.13399 12.7321C7.39729 12.9076 7.69597 13 8 13C8.30403 13 8.60271 12.9076 8.86601 12.7321C9.12932 12.5565 9.34797 12.304 9.5 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.63088 9.21874C3.56556 9.28556 3.52246 9.36865 3.50681 9.45791C3.49116 9.54718 3.50364 9.63876 3.54273 9.72152C3.58183 9.80429 3.64585 9.87467 3.72701 9.92409C3.80817 9.97352 3.90298 9.99987 3.99989 9.99994H12.0001C12.097 9.99997 12.1918 9.97372 12.273 9.92439C12.3542 9.87505 12.4183 9.80476 12.4575 9.72205C12.4967 9.63934 12.5093 9.54778 12.4938 9.45851C12.4783 9.36924 12.4353 9.2861 12.3701 9.21921C11.705 8.57941 11 7.89947 11 5.79994C11 5.05733 10.684 4.34514 10.1213 3.82004C9.55872 3.29494 8.79564 2.99994 7.99997 2.99994C7.20431 2.99994 6.44123 3.29494 5.87861 3.82004C5.31599 4.34514 4.99991 5.05733 4.99991 5.79994C4.99991 7.89947 4.2944 8.57941 3.63088 9.21874Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5L11 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 835 B

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5.66667V4.33333C3 3.97971 3.14048 3.64057 3.39052 3.39052C3.64057 3.14048 3.97971 3 4.33333 3H5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.3333 3H11.6666C12.0202 3 12.3593 3.14048 12.6094 3.39052C12.8594 3.64057 12.9999 3.97971 12.9999 4.33333V5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.9999 10.3333V11.6666C12.9999 12.0203 12.8594 12.3594 12.6094 12.6095C12.3593 12.8595 12.0202 13 11.6666 13H10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.66667 13H4.33333C3.97971 13 3.64057 12.8595 3.39052 12.6095C3.14048 12.3594 3 12.0203 3 11.6666V10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 8H10.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.57132 13.7143C5.20251 13.7143 5.71418 13.2026 5.71418 12.5714C5.71418 11.9403 5.20251 11.4286 4.57132 11.4286C3.94014 11.4286 3.42847 11.9403 3.42847 12.5714C3.42847 13.2026 3.94014 13.7143 4.57132 13.7143Z" fill="black"/>
<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 13L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.99487 8.44023L7.32821 7.10689L5.99487 5.77356" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.33838 10.2264H10.005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 625 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99993 13.4804C11.0267 13.4804 13.4803 11.0267 13.4803 7.99999C13.4803 4.97325 11.0267 2.51959 7.99993 2.51959C4.97319 2.51959 2.51953 4.97325 2.51953 7.99999C2.51953 11.0267 4.97319 13.4804 7.99993 13.4804Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 3C6.71611 4.34807 6 6.13836 6 7.99999C6 9.86163 6.71611 11.6519 8 13C9.28387 11.6519 10 9.86163 10 7.99999C10 6.13836 9.28387 4.34807 8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.24121 7.04827C4.52425 8.27022 6.22817 8.95178 7.99999 8.95178C9.77182 8.95178 11.4757 8.27022 12.7588 7.04827" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 847 B

View File

@@ -268,6 +268,14 @@
"ctrl-alt-t": "agent::NewThread"
}
},
{
"context": "AgentPanel && acp_thread",
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "agent::NewAcpThread",
"ctrl-alt-t": "agent::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
@@ -306,6 +314,15 @@
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage"
}
},
{
"context": "ThreadHistory",
"bindings": {

View File

@@ -309,6 +309,14 @@
"cmd-alt-t": "agent::NewThread"
}
},
{
"context": "AgentPanel && acp_thread",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agent::NewAcpThread",
"cmd-alt-t": "agent::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
@@ -357,6 +365,15 @@
"ctrl--": "pane::GoBack"
}
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage"
}
},
{
"context": "ThreadHistory",
"bindings": {

View File

@@ -362,7 +362,9 @@
// Whether to show user picture in the titlebar.
"show_user_picture": true,
// Whether to show the sign in button in the titlebar.
"show_sign_in": true
"show_sign_in": true,
// Whether to show the menus in the titlebar.
"show_menus": false
},
// Scrollbar related settings
"scrollbar": {
@@ -1855,6 +1857,8 @@
"read_ssh_config": true,
// Configures context servers for use by the agent.
"context_servers": {},
// Configures agent servers available in the agent panel.
"agent_servers": {},
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,

46
crates/acp/Cargo.toml Normal file
View File

@@ -0,0 +1,46 @@
[package]
name = "acp"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/acp.rs"
doctest = false
[features]
test-support = ["gpui/test-support", "project/test-support"]
gemini = []
[dependencies]
agent_servers.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
markdown.workspace = true
project.workspace = true
settings.workspace = true
smol.workspace = true
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
async-pipe.workspace = true
env_logger.workspace = true
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
project = { workspace = true, "features" = ["test-support"] }
serde_json.workspace = true
tempfile.workspace = true
util.workspace = true
settings.workspace = true

1
crates/acp/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

1645
crates/acp/src/acp.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1284,6 +1284,7 @@ impl Thread {
tool_choice: None,
stop: Vec::new(),
temperature: AgentSettings::temperature_for_model(&model, cx),
thinking_allowed: true,
};
let available_tools = self.available_tools(cx, model.clone());
@@ -1449,6 +1450,7 @@ impl Thread {
tool_choice: None,
stop: Vec::new(),
temperature: AgentSettings::temperature_for_model(model, cx),
thinking_allowed: false,
};
for message in &self.messages {

View File

@@ -0,0 +1,27 @@
[package]
name = "agent_servers"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/agent_servers.rs"
doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
paths.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
util.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,231 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AsyncApp, Entity, SharedString};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use util::{ResultExt, paths};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
pub struct AllAgentServersSettings {
gemini: Option<AgentServerSettings>,
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
pub struct AgentServerSettings {
#[serde(flatten)]
command: AgentServerCommand,
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct AgentServerCommand {
#[serde(rename = "command")]
pub path: PathBuf,
#[serde(default)]
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
}
pub struct Gemini;
pub struct AgentServerVersion {
pub current_version: SharedString,
pub supported: bool,
}
pub trait AgentServer: Send {
fn command(
&self,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> impl Future<Output = Result<AgentServerCommand>>;
fn version(
&self,
command: &AgentServerCommand,
) -> impl Future<Output = Result<AgentServerVersion>> + Send;
}
const GEMINI_ACP_ARG: &str = "--acp";
impl AgentServer for Gemini {
async fn command(
&self,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Result<AgentServerCommand> {
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
let settings = settings.get::<AllAgentServersSettings>(None);
settings
.gemini
.as_ref()
.map(|gemini_settings| AgentServerCommand {
path: gemini_settings.command.path.clone(),
args: gemini_settings
.command
.args
.iter()
.cloned()
.chain(std::iter::once(GEMINI_ACP_ARG.into()))
.collect(),
env: gemini_settings.command.env.clone(),
})
})?;
if let Some(custom_command) = custom_command {
return Ok(custom_command);
}
if let Some(path) = find_bin_in_path("gemini", project, cx).await {
return Ok(AgentServerCommand {
path,
args: vec![GEMINI_ACP_ARG.into()],
env: None,
});
}
let (fs, node_runtime) = project.update(cx, |project, _| {
(project.fs().clone(), project.node_runtime().cloned())
})?;
let node_runtime = node_runtime.context("gemini not found on path")?;
let directory = ::paths::agent_servers_dir().join("gemini");
fs.create_dir(&directory).await?;
node_runtime
.npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
.await?;
let path = directory.join("node_modules/.bin/gemini");
Ok(AgentServerCommand {
path,
args: vec![GEMINI_ACP_ARG.into()],
env: None,
})
}
async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
let current_version = String::from_utf8(version_output?.stdout)?.into();
let supported = String::from_utf8(help_output?.stdout)?.contains(GEMINI_ACP_ARG);
Ok(AgentServerVersion {
current_version,
supported,
})
}
}
async fn find_bin_in_path(
bin_name: &'static str,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<PathBuf> {
let (env_task, root_dir) = project
.update(cx, |project, cx| {
let worktree = project.visible_worktrees(cx).next();
match worktree {
Some(worktree) => {
let env_task = project.environment().update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
});
let path = worktree.read(cx).abs_path();
(env_task, path)
}
None => {
let path: Arc<Path> = paths::home_dir().as_path().into();
let env_task = project.environment().update(cx, |env, cx| {
env.get_directory_environment(path.clone(), cx)
});
(env_task, path)
}
}
})
.log_err()?;
cx.background_executor()
.spawn(async move {
let which_result = if cfg!(windows) {
which::which(bin_name)
} else {
let env = env_task.await.unwrap_or_default();
let shell_path = env.get("PATH").cloned();
which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
};
if let Err(which::Error::CannotFindBinaryPath) = which_result {
return None;
}
which_result.log_err()
})
.await
}
impl std::fmt::Debug for AgentServerCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let filtered_env = self.env.as_ref().map(|env| {
env.iter()
.map(|(k, v)| {
(
k,
if util::redact::should_redact(k) {
"[REDACTED]"
} else {
v
},
)
})
.collect::<Vec<_>>()
});
f.debug_struct("AgentServerCommand")
.field("path", &self.path)
.field("args", &self.args)
.field("env", &filtered_env)
.finish()
}
}
impl settings::Settings for AllAgentServersSettings {
const KEY: Option<&'static str> = Some("agent_servers");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for value in sources.defaults_and_customizations() {
if value.gemini.is_some() {
settings.gemini = value.gemini.clone();
}
}
Ok(settings)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -13,14 +13,14 @@ path = "src/agent_ui.rs"
doctest = false
[features]
test-support = [
"gpui/test-support",
"language/test-support",
]
test-support = ["gpui/test-support", "language/test-support"]
[dependencies]
acp.workspace = true
agent.workspace = true
agentic-coding-protocol.workspace = true
agent_settings.workspace = true
agent_servers.workspace = true
anyhow.workspace = true
assistant_context.workspace = true
assistant_slash_command.workspace = true
@@ -76,6 +76,7 @@ serde_json_lenient.workspace = true
settings.workspace = true
smol.workspace = true
streaming_diff.workspace = true
task.workspace = true
telemetry.workspace = true
telemetry_events.workspace = true
terminal.workspace = true

View File

@@ -0,0 +1,5 @@
mod completion_provider;
mod message_history;
mod thread_view;
pub use thread_view::AcpThreadView;

View File

@@ -0,0 +1,574 @@
use std::ops::Range;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use collections::HashMap;
use editor::display_map::CreaseId;
use editor::{CompletionProvider, Editor, ExcerptId};
use file_icons::FileIcons;
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use parking_lot::Mutex;
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId};
use rope::Point;
use text::{Anchor, ToPoint};
use ui::prelude::*;
use workspace::Workspace;
use crate::context_picker::MentionLink;
use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files};
#[derive(Default)]
pub struct MentionSet {
paths_by_crease_id: HashMap<CreaseId, ProjectPath>,
}
impl MentionSet {
pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) {
self.paths_by_crease_id.insert(crease_id, path);
}
pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option<ProjectPath> {
self.paths_by_crease_id.get(&crease_id).cloned()
}
pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
self.paths_by_crease_id.drain().map(|(id, _)| id)
}
}
pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
}
impl ContextPickerCompletionProvider {
pub fn new(
mention_set: Arc<Mutex<MentionSet>>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
) -> Self {
Self {
mention_set,
workspace,
editor,
}
}
fn completion_for_path(
project_path: ProjectPath,
path_prefix: &str,
is_recent: bool,
is_directory: bool,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
cx: &App,
) -> Completion {
let (file_name, directory) =
extract_file_name_and_directory(&project_path.path, path_prefix);
let label =
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
} else {
file_name.to_string()
};
let crease_icon_path = if is_directory {
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
} else {
FileIcons::get_icon(Path::new(&full_path), cx)
.unwrap_or_else(|| IconName::File.path().into())
};
let completion_icon_path = if is_recent {
IconName::HistoryRerun.path().into()
} else {
crease_icon_path.clone()
};
let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path));
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
new_text,
label,
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
crease_icon_path,
file_name,
project_path,
excerpt_id,
source_range.start,
new_text_len - 1,
editor,
mention_set,
)),
}
}
}
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
label.push_str(&file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
label.push_str(&directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
}
impl CompletionProvider for ContextPickerCompletionProvider {
fn completions(
&self,
excerpt_id: ExcerptId,
buffer: &Entity<Buffer>,
buffer_position: Anchor,
_trigger: CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
let state = buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
MentionCompletion::try_parse(line, offset_to_line)
});
let Some(state) = state else {
return Task::ready(Ok(Vec::new()));
};
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Ok(Vec::new()));
};
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
let editor = self.editor.clone();
let mention_set = self.mention_set.clone();
let MentionCompletion { argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
let search_task = search_files(query.clone(), Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let Some(editor) = editor.upgrade() else {
return Ok(Vec::new());
};
let completions = cx.update(|cx| {
matches
.into_iter()
.map(|mat| {
let path_match = &mat.mat;
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(path_match.worktree_id),
path: path_match.path.clone(),
};
Self::completion_for_path(
project_path,
&path_match.path_prefix,
mat.is_recent,
path_match.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
mention_set.clone(),
cx,
)
})
.collect()
})?;
Ok(vec![CompletionResponse {
completions,
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
}
fn is_completion_trigger(
&self,
buffer: &Entity<language::Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
MentionCompletion::try_parse(line, offset_to_line)
.map(|completion| {
completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize
})
.unwrap_or(false)
} else {
false
}
}
fn sort_completions(&self) -> bool {
false
}
fn filter_completions(&self) -> bool {
false
}
}
fn confirm_completion_callback(
crease_icon_path: SharedString,
crease_text: SharedString,
project_path: ProjectPath,
excerpt_id: ExcerptId,
start: Anchor,
content_len: usize,
editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, window, cx| {
let crease_text = crease_text.clone();
let crease_icon_path = crease_icon_path.clone();
let editor = editor.clone();
let project_path = project_path.clone();
let mention_set = mention_set.clone();
window.defer(cx, move |window, cx| {
let crease_id = crate::context_picker::insert_crease_for_mention(
excerpt_id,
start,
content_len,
crease_text.clone(),
crease_icon_path,
editor.clone(),
window,
cx,
);
if let Some(crease_id) = crease_id {
mention_set.lock().insert(crease_id, project_path);
}
});
false
})
}
#[derive(Debug, Default, PartialEq)]
struct MentionCompletion {
source_range: Range<usize>,
argument: Option<String>,
}
impl MentionCompletion {
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
let last_mention_start = line.rfind('@')?;
if last_mention_start >= line.len() {
return Some(Self::default());
}
if last_mention_start > 0
&& line
.chars()
.nth(last_mention_start - 1)
.map_or(false, |c| !c.is_whitespace())
{
return None;
}
let rest_of_line = &line[last_mention_start + 1..];
let mut argument = None;
let mut parts = rest_of_line.split_whitespace();
let mut end = last_mention_start + 1;
if let Some(argument_text) = parts.next() {
end += argument_text.len();
argument = Some(argument_text.to_string());
}
Some(Self {
source_range: last_mention_start + offset_to_line..end + offset_to_line,
argument,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
use project::{Project, ProjectPath};
use serde_json::json;
use settings::SettingsStore;
use std::{ops::Deref, rc::Rc};
use util::path;
use workspace::{AppState, Item};
#[test]
fn test_mention_completion_parse() {
assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
assert_eq!(
MentionCompletion::try_parse("Lorem @", 0),
Some(MentionCompletion {
source_range: 6..7,
argument: None,
})
);
assert_eq!(
MentionCompletion::try_parse("Lorem @main", 0),
Some(MentionCompletion {
source_range: 6..11,
argument: Some("main".to_string()),
})
);
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
}
struct AtMentionEditor(Entity<Editor>);
impl Item for AtMentionEditor {
type Event = ();
fn include_in_nav_history() -> bool {
false
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Test".into()
}
}
impl EventEmitter<()> for AtMentionEditor {}
impl Focusable for AtMentionEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.0.read(cx).focus_handle(cx).clone()
}
}
impl Render for AtMentionEditor {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.0.clone().into_any_element()
}
}
#[gpui::test]
async fn test_context_completion_provider(cx: &mut TestAppContext) {
init_test(cx);
let app_state = cx.update(AppState::test);
cx.update(|cx| {
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
});
app_state
.fs
.as_fake()
.insert_tree(
path!("/dir"),
json!({
"editor": "",
"a": {
"one.txt": "",
"two.txt": "",
"three.txt": "",
"four.txt": ""
},
"b": {
"five.txt": "",
"six.txt": "",
"seven.txt": "",
"eight.txt": "",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
let paths = vec![
path!("a/one.txt"),
path!("a/two.txt"),
path!("a/three.txt"),
path!("a/four.txt"),
path!("b/five.txt"),
path!("b/six.txt"),
path!("b/seven.txt"),
path!("b/eight.txt"),
];
let mut opened_editors = Vec::new();
for path in paths {
let buffer = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.open_path(
ProjectPath {
worktree_id,
path: Path::new(path).into(),
},
None,
false,
window,
cx,
)
})
.await
.unwrap();
opened_editors.push(buffer);
}
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
let editor = cx.new(|cx| {
Editor::new(
editor::EditorMode::full(),
multi_buffer::MultiBuffer::build_simple("", cx),
None,
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
true,
true,
None,
window,
cx,
);
});
editor
});
let mention_set = Arc::new(Mutex::new(MentionSet::default()));
let editor_entity = editor.downgrade();
editor.update_in(&mut cx, |editor, window, cx| {
window.focus(&editor.focus_handle(cx));
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
mention_set.clone(),
workspace.downgrade(),
editor_entity,
))));
});
cx.simulate_input("Lorem ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem ");
assert!(!editor.has_visible_completions_menu());
});
cx.simulate_input("@");
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels(editor),
&[
"eight.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"four.txt dir/a/",
"three.txt dir/a/",
"two.txt dir/a/",
"one.txt dir/a/",
"dir ",
"a dir/",
"four.txt dir/a/",
"one.txt dir/a/",
"three.txt dir/a/",
"two.txt dir/a/",
"b dir/",
"eight.txt dir/b/",
"five.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"editor dir/"
]
);
});
// Select and confirm "File"
editor.update_in(&mut cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem [@four.txt](@file:dir/a/four.txt) ");
});
}
fn current_completion_labels(editor: &Editor) -> Vec<String> {
let completions = editor.current_completions().expect("Missing completions");
completions
.into_iter()
.map(|completion| completion.label.text.to_string())
.collect::<Vec<_>>()
}
pub(crate) fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
editor::init_settings(cx);
});
}
}

View File

@@ -0,0 +1,81 @@
pub struct MessageHistory<T> {
items: Vec<T>,
current: Option<usize>,
}
impl<T> MessageHistory<T> {
pub fn new() -> Self {
MessageHistory {
items: Vec::new(),
current: None,
}
}
pub fn push(&mut self, message: T) {
self.current.take();
self.items.push(message);
}
pub fn prev(&mut self) -> Option<&T> {
if self.items.is_empty() {
return None;
}
let new_ix = self
.current
.get_or_insert(self.items.len())
.saturating_sub(1);
self.current = Some(new_ix);
self.items.get(new_ix)
}
pub fn next(&mut self) -> Option<&T> {
let current = self.current.as_mut()?;
*current += 1;
self.items.get(*current).or_else(|| {
self.current.take();
None
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prev_next() {
let mut history = MessageHistory::new();
// Test empty history
assert_eq!(history.prev(), None);
assert_eq!(history.next(), None);
// Add some messages
history.push("first");
history.push("second");
history.push("third");
// Test prev navigation
assert_eq!(history.prev(), Some(&"third"));
assert_eq!(history.prev(), Some(&"second"));
assert_eq!(history.prev(), Some(&"first"));
assert_eq!(history.prev(), Some(&"first"));
assert_eq!(history.next(), Some(&"second"));
// Test mixed navigation
history.push("fourth");
assert_eq!(history.prev(), Some(&"fourth"));
assert_eq!(history.prev(), Some(&"third"));
assert_eq!(history.next(), Some(&"fourth"));
assert_eq!(history.next(), None);
// Test that push resets navigation
history.prev();
history.prev();
history.push("fifth");
assert_eq!(history.prev(), Some(&"fifth"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1461,6 +1461,7 @@ impl ActiveThread {
&configured_model.model,
cx,
),
thinking_allowed: true,
};
Some(configured_model.model.count_tokens(request, cx))
@@ -2580,8 +2581,8 @@ impl ActiveThread {
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::LightBulb)
.size(IconSize::XSmall)
Icon::new(IconName::ToolBulb)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(LoadingLabel::new("Thinking").size(LabelSize::Small)),
@@ -2994,7 +2995,7 @@ impl ActiveThread {
.overflow_x_scroll()
.child(
Icon::new(tool_use.icon)
.size(IconSize::XSmall)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(

View File

@@ -7,12 +7,14 @@ use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
use crate::NewAcpThread;
use crate::language_model_selector::ToggleModelSelector;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
acp::AcpThreadView,
active_thread::{self, ActiveThread, ActiveThreadEvent},
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
agent_diff::AgentDiff,
@@ -38,6 +40,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::{UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use feature_flags::{self, FeatureFlagAppExt};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
@@ -109,6 +112,12 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
}
})
.register_action(|workspace, _: &NewAcpThread, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.new_gemini_thread(window, cx));
}
})
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -125,7 +134,8 @@ pub fn init(cx: &mut App) {
let thread = thread.read(cx).thread().clone();
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
ActiveView::TextThread { .. }
ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
}
@@ -188,6 +198,9 @@ enum ActiveView {
message_editor: Entity<MessageEditor>,
_subscriptions: Vec<gpui::Subscription>,
},
AcpThread {
thread_view: Entity<AcpThreadView>,
},
TextThread {
context_editor: Entity<TextThreadEditor>,
title_editor: Entity<Editor>,
@@ -207,7 +220,9 @@ enum WhichFontSize {
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
ActiveView::Thread { .. } | ActiveView::AcpThread { .. } | ActiveView::History => {
WhichFontSize::AgentFont
}
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
ActiveView::Configuration => WhichFontSize::None,
}
@@ -238,6 +253,7 @@ impl ActiveView {
thread.scroll_to_bottom(cx);
});
}
ActiveView::AcpThread { .. } => {}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -420,7 +436,8 @@ pub struct AgentPanel {
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
hovered_recent_history_item: Option<usize>,
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu: Option<Entity<ContextMenu>>,
width: Option<Pixels>,
@@ -653,7 +670,8 @@ impl AgentPanel {
.clone()
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
}
ActiveView::TextThread { .. }
ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
},
@@ -683,7 +701,8 @@ impl AgentPanel {
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
hovered_recent_history_item: None,
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu: None,
width: None,
@@ -733,6 +752,9 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => {
thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
}
ActiveView::AcpThread { thread_view, .. } => {
thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -740,7 +762,10 @@ impl AgentPanel {
fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
match &self.active_view {
ActiveView::Thread { message_editor, .. } => Some(message_editor),
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
}
}
@@ -862,6 +887,21 @@ impl AgentPanel {
context_editor.focus_handle(cx).focus(window);
}
fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let workspace = self.workspace.clone();
let project = self.project.clone();
cx.spawn_in(window, async move |this, cx| {
let thread_view = cx.new_window_entity(|window, cx| {
crate::acp::AcpThreadView::new(workspace, project, window, cx)
})?;
this.update_in(cx, |this, window, cx| {
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
})
})
.detach();
}
fn deploy_rules_library(
&mut self,
action: &OpenRulesLibrary,
@@ -994,6 +1034,7 @@ impl AgentPanel {
cx,
)
});
let message_editor = cx.new(|cx| {
MessageEditor::new(
self.fs.clone(),
@@ -1025,6 +1066,9 @@ impl AgentPanel {
ActiveView::Thread { message_editor, .. } => {
message_editor.focus_handle(cx).focus(window);
}
ActiveView::AcpThread { thread_view } => {
thread_view.focus_handle(cx).focus(window);
}
ActiveView::TextThread { context_editor, .. } => {
context_editor.focus_handle(cx).focus(window);
}
@@ -1052,7 +1096,7 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.assistant_dropdown_menu_handle.toggle(window, cx);
self.agent_panel_menu_handle.toggle(window, cx);
}
pub fn increase_font_size(
@@ -1144,7 +1188,10 @@ impl AgentPanel {
})
.log_err();
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
}
}
@@ -1197,6 +1244,13 @@ impl AgentPanel {
)
.detach_and_log_err(cx);
}
ActiveView::AcpThread { thread_view } => {
thread_view
.update(cx, |thread_view, cx| {
thread_view.open_thread_as_markdown(workspace, window, cx)
})
.detach_and_log_err(cx);
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -1351,7 +1405,8 @@ impl AgentPanel {
}
})
}
_ => {}
ActiveView::AcpThread { .. } => {}
ActiveView::History | ActiveView::Configuration => {}
}
if current_is_special && !new_is_special {
@@ -1437,6 +1492,7 @@ impl Focusable for AgentPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.active_view {
ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
ActiveView::AcpThread { thread_view, .. } => thread_view.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
ActiveView::Configuration => {
@@ -1593,6 +1649,9 @@ impl AgentPanel {
.into_any_element(),
}
}
ActiveView::AcpThread { thread_view } => Label::new(thread_view.read(cx).title(cx))
.truncate()
.into_any_element(),
ActiveView::TextThread {
title_editor,
context_editor,
@@ -1727,10 +1786,51 @@ impl AgentPanel {
let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
};
let agent_extra_menu = PopoverMenu::new("agent-options-menu")
let new_thread_menu = PopoverMenu::new("new_thread_menu")
.trigger_with_tooltip(
IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
Tooltip::text("New Thread…"),
)
.anchor(Corner::TopRight)
.with_handle(self.new_thread_menu_handle.clone())
.menu(move |window, cx| {
let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.header("Zed Agent")
})
.action("New Thread", NewThread::default().boxed_clone())
.action("New Text Thread", NewTextThread.boxed_clone())
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() {
this.action(
"New From Summary",
Box::new(NewThread {
from_thread_id: Some(thread.id().clone()),
}),
)
} else {
this
}
})
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.separator()
.header("External Agents")
.action("New Gemini Thread", NewAcpThread.boxed_clone())
});
menu
}))
});
let agent_panel_menu = PopoverMenu::new("agent-options-menu")
.trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis)
.icon_size(IconSize::Small),
@@ -1748,41 +1848,9 @@ impl AgentPanel {
},
)
.anchor(Corner::TopRight)
.with_handle(self.assistant_dropdown_menu_handle.clone())
.with_handle(self.agent_panel_menu_handle.clone())
.menu(move |window, cx| {
let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu
.action("New Thread", NewThread::default().boxed_clone())
.action("New Text Thread", NewTextThread.boxed_clone())
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() {
this.action(
"New From Summary",
Box::new(NewThread {
from_thread_id: Some(thread.id().clone()),
}),
)
} else {
this
}
})
.separator();
menu = menu
.header("MCP Servers")
.action(
"View Server Extensions",
Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
}),
)
.action("Add Custom Server…", Box::new(AddContextServer))
.separator();
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
if let Some(usage) = usage {
menu = menu
.header_with_link("Prompt Usage", "Manage", account_url.clone())
@@ -1820,6 +1888,19 @@ impl AgentPanel {
.separator()
}
menu = menu
.header("MCP Servers")
.action(
"View Server Extensions",
Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
}),
)
.action("Add Custom Server…", Box::new(AddContextServer))
.separator();
menu = menu
.action("Rules…", Box::new(OpenRulesLibrary::default()))
.action("Settings", Box::new(OpenConfiguration))
@@ -1861,27 +1942,8 @@ impl AgentPanel {
.px(DynamicSpacing::Base08.rems(cx))
.border_l_1()
.border_color(cx.theme().colors().border)
.child(
IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"New Thread",
&NewThread::default(),
&focus_handle,
window,
cx,
)
})
.on_click(move |_event, window, cx| {
window.dispatch_action(
NewThread::default().boxed_clone(),
cx,
);
}),
)
.child(agent_extra_menu),
.child(new_thread_menu)
.child(agent_panel_menu),
),
)
}
@@ -1893,6 +1955,9 @@ impl AgentPanel {
message_editor,
..
} => (thread.read(cx), message_editor.read(cx)),
ActiveView::AcpThread { .. } => {
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
@@ -2031,6 +2096,9 @@ impl AgentPanel {
return false;
}
}
ActiveView::AcpThread { .. } => {
return false;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return false;
}
@@ -2615,6 +2683,9 @@ impl AgentPanel {
) -> Option<AnyElement> {
let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => thread,
ActiveView::AcpThread { .. } => {
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
@@ -2961,6 +3032,9 @@ impl AgentPanel {
.detach();
});
}
ActiveView::AcpThread { .. } => {
unimplemented!()
}
ActiveView::TextThread { context_editor, .. } => {
context_editor.update(cx, |context_editor, cx| {
TextThreadEditor::insert_dragged_files(
@@ -2979,8 +3053,10 @@ impl AgentPanel {
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
if matches!(self.active_view, ActiveView::TextThread { .. }) {
key_context.add("prompt_editor");
match &self.active_view {
ActiveView::AcpThread { .. } => key_context.add("acp_thread"),
ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
key_context
}
@@ -3034,6 +3110,7 @@ impl Render for AgentPanel {
});
this.continue_conversation(window, cx);
}
ActiveView::AcpThread { .. } => {}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -3075,6 +3152,10 @@ impl Render for AgentPanel {
})
.child(h_flex().child(message_editor.clone()))
.child(self.render_drag_target(cx)),
ActiveView::AcpThread { thread_view, .. } => parent
.relative()
.child(thread_view.clone())
.child(self.render_drag_target(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::TextThread {
context_editor,

View File

@@ -1,3 +1,4 @@
mod acp;
mod active_thread;
mod agent_configuration;
mod agent_diff;
@@ -56,6 +57,8 @@ actions!(
[
/// Creates a new text-based conversation thread.
NewTextThread,
/// Creates a new external agent conversation thread.
NewAcpThread,
/// Toggles the context picker interface for adding files, symbols, or other context.
ToggleContextPicker,
/// Toggles the navigation menu for switching between threads and views.
@@ -76,8 +79,6 @@ actions!(
AddContextServer,
/// Removes the currently selected thread.
RemoveSelectedThread,
/// Starts a chat conversation with the agent.
Chat,
/// Starts a chat conversation with follow-up enabled.
ChatWithFollow,
/// Cycles to the next inline assist suggestion.

View File

@@ -475,6 +475,7 @@ impl CodegenAlternative {
stop: Vec::new(),
temperature,
messages: vec![request_message],
thinking_allowed: false,
}
}))
}

View File

@@ -1,6 +1,6 @@
mod completion_provider;
mod fetch_context_picker;
mod file_context_picker;
pub(crate) mod file_context_picker;
mod rules_context_picker;
mod symbol_context_picker;
mod thread_context_picker;

View File

@@ -47,13 +47,14 @@ use ui::{
};
use util::ResultExt as _;
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::Chat;
use zed_llm_client::CompletionIntent;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::{
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
};
@@ -1453,6 +1454,7 @@ impl MessageEditor {
tool_choice: None,
stop: vec![],
temperature: AgentSettings::temperature_for_model(&model.model, cx),
thinking_allowed: true,
};
Some(model.model.count_tokens(request, cx))
@@ -1620,6 +1622,7 @@ impl Render for MessageEditor {
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(changed_buffers.len() > 0, |parent| {
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
})

View File

@@ -297,6 +297,7 @@ impl TerminalInlineAssistant {
tool_choice: None,
stop: Vec::new(),
temperature,
thinking_allowed: false,
}
}))
}

View File

@@ -2293,6 +2293,7 @@ impl AssistantContext {
tool_choice: None,
stop: Vec::new(),
temperature: model.and_then(|model| AgentSettings::temperature_for_model(model, cx)),
thinking_allowed: true,
};
for message in self.messages(cx) {
if message.status != MessageStatus::Done {

View File

@@ -57,7 +57,7 @@ impl Tool for CopyPathTool {
}
fn icon(&self) -> IconName {
IconName::Clipboard
IconName::ToolCopy
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -46,7 +46,7 @@ impl Tool for CreateDirectoryTool {
}
fn icon(&self) -> IconName {
IconName::Folder
IconName::ToolFolder
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -46,7 +46,7 @@ impl Tool for DeletePathTool {
}
fn icon(&self) -> IconName {
IconName::FileDelete
IconName::ToolDeleteFile
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -59,7 +59,7 @@ impl Tool for DiagnosticsTool {
}
fn icon(&self) -> IconName {
IconName::XCircle
IconName::ToolDiagnostics
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -719,6 +719,7 @@ impl EditAgent {
tools,
stop: Vec::new(),
temperature: None,
thinking_allowed: true,
};
Ok(self.model.stream_completion_text(request, cx).await?.stream)

View File

@@ -1263,6 +1263,7 @@ impl EvalAssertion {
content: vec![prompt.into()],
cache: false,
}],
thinking_allowed: true,
..Default::default()
};
let mut response = retry_on_rate_limit(async || {
@@ -1599,6 +1600,7 @@ impl EditAgentTest {
let conversation = LanguageModelRequest {
messages,
tools,
thinking_allowed: true,
..Default::default()
};

View File

@@ -139,7 +139,7 @@ impl Tool for EditFileTool {
}
fn icon(&self) -> IconName {
IconName::Pencil
IconName::ToolPencil
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -783,8 +783,8 @@ impl ToolCard for EditFileToolCard {
.child(
h_flex()
.child(
Icon::new(IconName::Pencil)
.size(IconSize::XSmall)
Icon::new(IconName::ToolPencil)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(

View File

@@ -130,7 +130,7 @@ impl Tool for FetchTool {
}
fn icon(&self) -> IconName {
IconName::Globe
IconName::ToolWeb
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -68,7 +68,7 @@ impl Tool for FindPathTool {
}
fn icon(&self) -> IconName {
IconName::SearchCode
IconName::ToolSearch
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -313,7 +313,7 @@ impl ToolCard for FindPathToolCard {
.mb_2()
.gap_1()
.child(
ToolCallCardHeader::new(IconName::SearchCode, matches_label)
ToolCallCardHeader::new(IconName::ToolSearch, matches_label)
.with_code_path(&self.glob)
.disclosure_slot(
Disclosure::new("path-search-disclosure", self.expanded)

View File

@@ -70,7 +70,7 @@ impl Tool for GrepTool {
}
fn icon(&self) -> IconName {
IconName::Regex
IconName::ToolRegex
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -58,7 +58,7 @@ impl Tool for ListDirectoryTool {
}
fn icon(&self) -> IconName {
IconName::Folder
IconName::ToolFolder
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -31,7 +31,7 @@ impl Tool for ProjectNotificationsTool {
}
fn icon(&self) -> IconName {
IconName::Envelope
IconName::ToolNotification
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
}
fn icon(&self) -> IconName {
IconName::FileSearch
IconName::ToolRead
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -90,7 +90,7 @@ impl Tool for TerminalTool {
}
fn icon(&self) -> IconName {
IconName::Terminal
IconName::ToolTerminal
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -37,7 +37,7 @@ impl Tool for ThinkingTool {
}
fn icon(&self) -> IconName {
IconName::LightBulb
IconName::ToolBulb
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -82,7 +82,7 @@ impl RenderOnce for ToolCallCardHeader {
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(self.icon)
.size(IconSize::XSmall)
.size(IconSize::Small)
.color(Color::Muted),
),
)

View File

@@ -143,6 +143,8 @@ impl ToolCard for WebSearchToolCard {
_workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let icon = IconName::ToolWeb;
let header = match self.response.as_ref() {
Some(Ok(response)) => {
let text: SharedString = if response.results.len() == 1 {
@@ -150,13 +152,12 @@ impl ToolCard for WebSearchToolCard {
} else {
format!("{} results", response.results.len()).into()
};
ToolCallCardHeader::new(IconName::Globe, "Searched the Web")
.with_secondary_text(text)
ToolCallCardHeader::new(icon, "Searched the Web").with_secondary_text(text)
}
Some(Err(error)) => {
ToolCallCardHeader::new(IconName::Globe, "Web Search").with_error(error.to_string())
ToolCallCardHeader::new(icon, "Web Search").with_error(error.to_string())
}
None => ToolCallCardHeader::new(IconName::Globe, "Searching the Web").loading(),
None => ToolCallCardHeader::new(icon, "Searching the Web").loading(),
};
let content = self.response.as_ref().and_then(|response| match response {

View File

@@ -16,13 +16,13 @@ doctest = false
test-support = [
"dap/test-support",
"dap_adapters/test-support",
"debugger_tools/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
"util/test-support",
"workspace/test-support",
"unindent",
"debugger_tools"
]
[dependencies]
@@ -69,7 +69,7 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
debugger_tools = { workspace = true, optional = true }
debugger_tools.workspace = true
unindent = { workspace = true, optional = true }
zed_actions.workspace = true

View File

@@ -622,6 +622,14 @@ impl DebugPanel {
.on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
.tooltip(Tooltip::text("Open Documentation"))
};
let logs_button = || {
IconButton::new("debug-open-logs", IconName::ScrollText)
.icon_size(IconSize::Small)
.on_click(move |_, window, cx| {
window.dispatch_action(debugger_tools::OpenDebugAdapterLogs.boxed_clone(), cx)
})
.tooltip(Tooltip::text("Open Debug Adapter Logs"))
};
Some(
div.border_b_1()
@@ -873,6 +881,7 @@ impl DebugPanel {
.justify_around()
.when(is_side, |this| {
this.child(new_session_button())
.child(logs_button())
.child(documentation_button())
}),
)
@@ -922,6 +931,7 @@ impl DebugPanel {
))
.when(!is_side, |this| {
this.child(new_session_button())
.child(logs_button())
.child(documentation_button())
}),
),

View File

@@ -4381,7 +4381,7 @@ impl Editor {
.take_while(|c| c.is_whitespace())
.count();
let comment_candidate = snapshot
.chars_for_range(range)
.chars_for_range(range.clone())
.skip(num_of_whitespaces)
.take(max_len_of_delimiter)
.collect::<String>();
@@ -4397,6 +4397,22 @@ impl Editor {
})
.max_by_key(|(_, len)| *len)?;
if let Some((block_start, _)) = language.block_comment_delimiters()
{
let block_start_trimmed = block_start.trim_end();
if block_start_trimmed.starts_with(delimiter.trim_end()) {
let line_content = snapshot
.chars_for_range(range)
.skip(num_of_whitespaces)
.take(block_start_trimmed.len())
.collect::<String>();
if line_content.starts_with(block_start_trimmed) {
return None;
}
}
}
let cursor_is_placed_after_comment_marker =
num_of_whitespaces + trimmed_len <= start_point.column as usize;
if cursor_is_placed_after_comment_marker {

View File

@@ -3080,6 +3080,45 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let lua_language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["--".into()],
block_comment: Some(("--[[".into(), "]]".into())),
..LanguageConfig::default()
},
None,
));
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
// Line with line comment should extend
cx.set_state(indoc! {"
--ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
--
--ˇ
"});
// Line with block comment that matches line comment should not extend
cx.set_state(indoc! {"
--[[ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
--[[
ˇ
"});
}
#[gpui::test]
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -324,20 +324,8 @@
<body>
<h1 id="current-filename">Thread Explorer</h1>
<div class="view-switcher">
<button
id="full-view"
class="view-button active"
onclick="switchView('full')"
>
Full View
</button>
<button
id="compact-view"
class="view-button"
onclick="switchView('compact')"
>
Compact View
</button>
<button id="full-view" class="view-button active" onclick="switchView('full')">Full View</button>
<button id="compact-view" class="view-button" onclick="switchView('compact')">Compact View</button>
<button
id="export-button"
class="view-button"
@@ -347,11 +335,7 @@
Export
</button>
<div class="theme-switcher">
<button
id="theme-toggle"
class="theme-button"
onclick="toggleTheme()"
>
<button id="theme-toggle" class="theme-button" onclick="toggleTheme()">
<span id="theme-icon" class="theme-icon">☀️</span>
<span id="theme-text">Light</span>
</button>
@@ -368,8 +352,7 @@
&larr; Previous
</button>
<div class="thread-indicator">
Thread <span id="current-thread-index">1</span> of
<span id="total-threads">1</span>:
Thread <span id="current-thread-index">1</span> of <span id="total-threads">1</span>:
<span id="thread-id">Default Thread</span>
</div>
<button
@@ -423,9 +406,7 @@
function toggleTheme() {
// If currently system or light, switch to dark
if (themeMode === "system") {
const systemDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
themeMode = systemDark ? "light" : "dark";
} else {
themeMode = themeMode === "light" ? "dark" : "light";
@@ -442,19 +423,15 @@
function initTheme() {
if (themeMode === "system") {
// Use system preference
const systemDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
applyTheme(systemDark ? "dark" : "light");
// Listen for system theme changes
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
if (themeMode === "system") {
applyTheme(e.matches ? "dark" : "light");
}
});
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (themeMode === "system") {
applyTheme(e.matches ? "dark" : "light");
}
});
} else {
// Use saved preference
applyTheme(themeMode);
@@ -466,49 +443,38 @@
viewMode = mode;
// Update button states
document
.getElementById("full-view")
.classList.toggle("active", mode === "full");
document
.getElementById("compact-view")
.classList.toggle("active", mode === "compact");
document.getElementById("full-view").classList.toggle("active", mode === "full");
document.getElementById("compact-view").classList.toggle("active", mode === "compact");
// Add or remove compact-mode class on the body
document.body.classList.toggle(
"compact-mode",
mode === "compact",
);
document.body.classList.toggle("compact-mode", mode === "compact");
// Re-render the thread with the new view mode
renderThread();
}
// Function to export the current thread as a JSON file
function exportThreadAsJson() {
// Clone the thread to avoid modifying the original
const threadToExport = JSON.parse(JSON.stringify(thread));
// Create a Blob with the JSON data
const blob = new Blob(
[JSON.stringify(threadToExport, null, 2)],
{ type: "application/json" }
);
const blob = new Blob([JSON.stringify(threadToExport, null, 2)], { type: "application/json" });
// Create a download link
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
// Generate filename based on thread ID or index
const filename = threadToExport.thread_id ||
threadToExport.filename ||
`thread-${currentThreadIndex + 1}.json`;
const filename =
threadToExport.thread_id || threadToExport.filename || `thread-${currentThreadIndex + 1}.json`;
a.download = filename.endsWith(".json") ? filename : `${filename}.json`;
// Trigger the download
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
document.body.removeChild(a);
@@ -524,9 +490,7 @@
},
{
role: "user",
content: [
{ Text: "Fix the bug: kwargs not passed..." },
],
content: [{ Text: "Fix the bug: kwargs not passed..." }],
},
{
role: "assistant",
@@ -593,12 +557,9 @@
name: "edit_file",
input: {
path: "fastmcp/core.py",
old_string:
"def start_server(app):\n anyio.run(app)",
new_string:
"def start_server(app, **kwargs):\n anyio.run(app, **kwargs)",
display_description:
"Fix kwargs passing to anyio.run",
old_string: "def start_server(app):\n anyio.run(app)",
new_string: "def start_server(app, **kwargs):\n anyio.run(app, **kwargs)",
display_description: "Fix kwargs passing to anyio.run",
},
is_input_complete: true,
},
@@ -681,14 +642,10 @@
// Function to update the navigation buttons state
function updateNavigationButtons() {
document.getElementById("prev-thread").disabled =
currentThreadIndex <= 0;
document.getElementById("next-thread").disabled =
currentThreadIndex >= threads.length - 1;
document.getElementById("current-thread-index").textContent =
currentThreadIndex + 1;
document.getElementById("total-threads").textContent =
threads.length;
document.getElementById("prev-thread").disabled = currentThreadIndex <= 0;
document.getElementById("next-thread").disabled = currentThreadIndex >= threads.length - 1;
document.getElementById("current-thread-index").textContent = currentThreadIndex + 1;
document.getElementById("total-threads").textContent = threads.length;
}
function renderThread() {
@@ -696,20 +653,15 @@
tbody.innerHTML = ""; // Clear existing content
// Set thread name if available
const threadId =
thread.thread_id || `Thread ${currentThreadIndex + 1}`;
const threadId = thread.thread_id || `Thread ${currentThreadIndex + 1}`;
document.getElementById("thread-id").textContent = threadId;
// Set filename in the header if available
const filename =
thread.filename || `Thread ${currentThreadIndex + 1}`;
document.getElementById("current-filename").textContent =
filename;
const filename = thread.filename || `Thread ${currentThreadIndex + 1}`;
document.getElementById("current-filename").textContent = filename;
// Skip system message
const nonSystemMessages = thread.messages.filter(
(msg) => msg.role !== "system",
);
const nonSystemMessages = thread.messages.filter((msg) => msg.role !== "system");
let turnNumber = 0;
processMessages(nonSystemMessages, tbody, turnNumber);
@@ -737,9 +689,7 @@
for (const content of msg.content) {
if (content.hasOwnProperty("Text")) {
if (assistantText) {
assistantText +=
"<br><br>" +
formatContent(content.Text);
assistantText += "<br><br>" + formatContent(content.Text);
} else {
assistantText = formatContent(content.Text);
}
@@ -763,49 +713,33 @@
tbody.appendChild(row);
// Add all tool calls to the tools cell
const toolsCell = document.getElementById(
`tools-${turnNumber}`,
);
const resultsCell = document.getElementById(
`results-${turnNumber}`,
);
const toolsCell = document.getElementById(`tools-${turnNumber}`);
const resultsCell = document.getElementById(`results-${turnNumber}`);
// Process all tools and their results
for (let j = 0; j < toolUses.length; j++) {
const toolUse = toolUses[j];
const toolCall = formatToolCall(
toolUse.name,
toolUse.input,
);
const toolCall = formatToolCall(toolUse.name, toolUse.input);
// Add the tool call to the tools cell
if (j > 0) toolsCell.innerHTML += "<hr>";
toolsCell.innerHTML += toolCall;
// Look for corresponding tool result
if (
hasMatchingToolResult(messages, i, toolUse.name)
) {
if (hasMatchingToolResult(messages, i, toolUse.name)) {
const resultMsg = messages[i + 1];
const toolResult = findToolResult(
resultMsg,
toolUse.name,
);
const toolResult = findToolResult(resultMsg, toolUse.name);
if (toolResult) {
// Add the result to the results cell
if (j > 0) resultsCell.innerHTML += "<hr>";
// Create a container for the result
const resultDiv =
document.createElement("div");
const resultDiv = document.createElement("div");
resultDiv.className = "tool-result";
// Format and display the tool result
formatToolResultInline(
toolResult.content,
resultDiv,
);
formatToolResultInline(toolResult.content.Text, resultDiv);
resultsCell.appendChild(resultDiv);
// Skip the result message in the next iteration
@@ -815,10 +749,7 @@
}
}
}
} else if (
msg.role === "user" &&
msg.content.some((c) => c.hasOwnProperty("ToolResult"))
) {
} else if (msg.role === "user" && msg.content.some((c) => c.hasOwnProperty("ToolResult"))) {
// Skip tool result messages as they are handled with their corresponding tool use
continue;
}
@@ -826,10 +757,7 @@
}
function isUserQuery(message) {
return (
message.role === "user" &&
!message.content.some((c) => c.hasOwnProperty("ToolResult"))
);
return message.role === "user" && !message.content.some((c) => c.hasOwnProperty("ToolResult"));
}
function renderUserMessage(message, turnNumber, tbody) {
@@ -848,18 +776,14 @@
currentIndex + 1 < messages.length &&
messages[currentIndex + 1].role === "user" &&
messages[currentIndex + 1].content.some(
(c) =>
c.hasOwnProperty("ToolResult") &&
c.ToolResult.tool_name === toolName,
(c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
)
);
}
function findToolResult(resultMessage, toolName) {
const toolResultContent = resultMessage.content.find(
(c) =>
c.hasOwnProperty("ToolResult") &&
c.ToolResult.tool_name === toolName,
(c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
);
return toolResultContent ? toolResultContent.ToolResult : null;
@@ -874,18 +798,12 @@
for (const [key, value] of Object.entries(input)) {
if (value !== null && value !== undefined) {
// Store full parameter for expanded view
let fullValue =
typeof value === "string"
? `"${value}"`
: value;
let fullValue = typeof value === "string" ? `"${value}"` : value;
fullParams.push([key, fullValue]);
// Abbreviated value for compact view
let displayValue = fullValue;
if (
typeof value === "string" &&
value.length > 30
) {
if (typeof value === "string" && value.length > 30) {
displayValue = `"${value.substring(0, 30)}..."`;
}
params.push(`${key}=${displayValue}`);
@@ -903,10 +821,7 @@
// For the full view, use the original untruncated values
let result = `<span class="tool-name">${name}</span>(`;
const formattedParams = fullParams
.map(
(p) =>
`&nbsp;&nbsp;&nbsp;&nbsp;${p[0]}=${p[1]}`,
)
.map((p) => `&nbsp;&nbsp;&nbsp;&nbsp;${p[0]}=${p[1]}`)
.join(",<br/>");
const fullView = `${result}<br/>${formattedParams}<br/>)`;
@@ -925,8 +840,7 @@
for (const [key, value] of Object.entries(input)) {
if (value !== null && value !== undefined) {
// Format different types of values
let formattedValue =
typeof value === "string" ? `"${value}"` : value;
let formattedValue = typeof value === "string" ? `"${value}"` : value;
params.push([key, formattedValue]);
}
}
@@ -938,9 +852,7 @@
return `${result}${params[0][1]})`;
} else {
// Format parameters
const formattedParams = params
.map((p) => `&nbsp;&nbsp;&nbsp;&nbsp;${p[0]}=${p[1]}`)
.join(",<br/>");
const formattedParams = params.map((p) => `&nbsp;&nbsp;&nbsp;&nbsp;${p[0]}=${p[1]}`).join(",<br/>");
return `${result}<br/>${formattedParams}<br/>)`;
}
}
@@ -1013,21 +925,13 @@
// Keyboard navigation handler
document.addEventListener("keydown", function (event) {
// previous thread
if (
(event.ctrlKey && event.key === "ArrowLeft") ||
event.key === "h" ||
event.key === "k"
) {
if ((event.ctrlKey && event.key === "ArrowLeft") || event.key === "h" || event.key === "k") {
if (!document.getElementById("prev-thread").disabled) {
previousThread();
}
}
// next thread
else if (
(event.ctrlKey && event.key === "ArrowRight") ||
event.key === "j" ||
event.key === "l"
) {
else if ((event.ctrlKey && event.key === "ArrowRight") || event.key === "j" || event.key === "l") {
if (!document.getElementById("next-thread").disabled) {
nextThread();
}

View File

@@ -594,6 +594,7 @@ impl ExampleInstance {
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
thinking_allowed: true,
};
let model = model.clone();

View File

@@ -718,24 +718,34 @@ impl ExtensionsPage {
}
parent.child(
h_flex().gap_2().children(
h_flex().gap_1().children(
extension
.manifest
.provides
.iter()
.map(|provides| {
div()
.bg(cx.theme().colors().element_background)
.px_0p5()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.child(
Label::new(extension_provides_label(
*provides,
))
.size(LabelSize::XSmall),
)
.filter_map(|provides| {
match provides {
ExtensionProvides::SlashCommands
| ExtensionProvides::IndexedDocsProviders => {
return None;
}
_ => {}
}
Some(
div()
.px_1()
.border_1()
.rounded_sm()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().element_background)
.child(
Label::new(extension_provides_label(
*provides,
))
.size(LabelSize::XSmall),
),
)
})
.collect::<Vec<_>>(),
),
@@ -744,8 +754,7 @@ impl ExtensionsPage {
)
.child(
h_flex()
.gap_2()
.justify_between()
.gap_1()
.children(buttons.upgrade)
.children(buttons.configure)
.child(buttons.install_or_uninstall),
@@ -1452,23 +1461,30 @@ impl Render for ExtensionsPage {
this.change_provides_filter(None, cx);
})),
)
.children(ExtensionProvides::iter().map(|provides| {
.children(ExtensionProvides::iter().filter_map(|provides| {
match provides {
ExtensionProvides::SlashCommands
| ExtensionProvides::IndexedDocsProviders => return None,
_ => {}
}
let label = extension_provides_label(provides);
Button::new(
SharedString::from(format!("filter-category-{}", label)),
label,
let button_id = SharedString::from(format!("filter-category-{}", label));
Some(
Button::new(button_id, label)
.style(if self.provides_filter == Some(provides) {
ButtonStyle::Filled
} else {
ButtonStyle::Subtle
})
.toggle_state(self.provides_filter == Some(provides))
.on_click({
cx.listener(move |this, _event, _, cx| {
this.change_provides_filter(Some(provides), cx);
})
}),
)
.style(if self.provides_filter == Some(provides) {
ButtonStyle::Filled
} else {
ButtonStyle::Subtle
})
.toggle_state(self.provides_filter == Some(provides))
.on_click({
cx.listener(move |this, _event, _, cx| {
this.change_provides_filter(Some(provides), cx);
})
})
})),
)
.child(self.render_feature_upsells(cx))

View File

@@ -92,6 +92,12 @@ impl FeatureFlag for JjUiFeatureFlag {
const NAME: &'static str = "jj-ui";
}
pub struct AcpFeatureFlag;
impl FeatureFlag for AcpFeatureFlag {
const NAME: &'static str = "acp";
}
pub struct ZedCloudFeatureFlag {}
impl FeatureFlag for ZedCloudFeatureFlag {

View File

@@ -1830,6 +1830,7 @@ impl GitPanel {
tool_choice: None,
stop: Vec::new(),
temperature,
thinking_allowed: false,
};
let stream = model.stream_completion_text(request, &cx);

View File

@@ -120,7 +120,18 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
}
RemoteAction::Push(branch_name, remote_ref) => {
if output.stderr.contains("* [new branch]") {
let style = if output.stderr.contains("Create a pull request") {
let pr_hints = [
// GitHub
"Create a pull request",
// Bitbucket
"Create pull request",
// GitLab
"create a merge request",
];
let style = if pr_hints
.iter()
.any(|indicator| output.stderr.contains(indicator))
{
let finder = LinkFinder::new();
let first_link = finder
.links(&output.stderr)
@@ -154,3 +165,109 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_push_new_branch_pull_request() {
let action = RemoteAction::Push(
SharedString::new("test_branch"),
Remote {
name: SharedString::new("test_remote"),
},
);
let output = RemoteCommandOutput {
stdout: String::new(),
stderr: String::from(
"
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote:
remote: Create a pull request for 'test' on GitHub by visiting:
remote: https://example.com/test/test/pull/new/test
remote:
To example.com:test/test.git
* [new branch] test -> test
",
),
};
let msg = format_output(&action, output);
if let SuccessStyle::PushPrLink { link } = &msg.style {
assert_eq!(link, "https://example.com/test/test/pull/new/test");
} else {
panic!("Expected PushPrLink variant");
}
}
#[test]
fn test_push_new_branch_merge_request() {
let action = RemoteAction::Push(
SharedString::new("test_branch"),
Remote {
name: SharedString::new("test_remote"),
},
);
let output = RemoteCommandOutput {
stdout: String::new(),
stderr: String::from("
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote:
remote: To create a merge request for test, visit:
remote: https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test
remote:
To example.com:test/test.git
* [new branch] test -> test
"),
};
let msg = format_output(&action, output);
if let SuccessStyle::PushPrLink { link } = &msg.style {
assert_eq!(
link,
"https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test"
);
} else {
panic!("Expected PushPrLink variant");
}
}
#[test]
fn test_push_new_branch_no_link() {
let action = RemoteAction::Push(
SharedString::new("test_branch"),
Remote {
name: SharedString::new("test_remote"),
},
);
let output = RemoteCommandOutput {
stdout: String::new(),
stderr: String::from(
"
To http://example.com/test/test.git
* [new branch] test -> test
",
),
};
let msg = format_output(&action, output);
if let SuccessStyle::ToastWithLog { output } = &msg.style {
assert_eq!(
output.stderr,
"
To http://example.com/test/test.git
* [new branch] test -> test
"
);
} else {
panic!("Expected ToastWithLog variant");
}
}
}

View File

@@ -272,6 +272,7 @@ pub struct App {
// TypeId is the type of the event that the listener callback expects
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>,
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
@@ -344,6 +345,7 @@ impl App {
event_listeners: SubscriberSet::new(),
release_listeners: SubscriberSet::new(),
keystroke_observers: SubscriberSet::new(),
keystroke_interceptors: SubscriberSet::new(),
keyboard_layout_observers: SubscriberSet::new(),
global_observers: SubscriberSet::new(),
quit_observers: SubscriberSet::new(),
@@ -1322,6 +1324,32 @@ impl App {
)
}
/// Register a callback to be invoked when a keystroke is received by the application
/// in any window. Note that this fires _before_ all other action and event mechanisms have resolved
/// unlike [`App::observe_keystrokes`] which fires after. This means that `cx.stop_propagation` calls
/// within interceptors will prevent action dispatch
pub fn intercept_keystrokes(
&mut self,
mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static,
) -> Subscription {
fn inner(
keystroke_interceptors: &SubscriberSet<(), KeystrokeObserver>,
handler: KeystrokeObserver,
) -> Subscription {
let (subscription, activate) = keystroke_interceptors.insert((), handler);
activate();
subscription
}
inner(
&mut self.keystroke_interceptors,
Box::new(move |event, window, cx| {
f(event, window, cx);
true
}),
)
}
/// Register key bindings.
pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
self.keymap.borrow_mut().add_bindings(bindings);

View File

@@ -1,6 +1,13 @@
#[cfg(any(feature = "wayland", feature = "x11"))]
use collections::{HashMap, HashSet};
#[cfg(any(feature = "wayland", feature = "x11"))]
use strum::{EnumIter, IntoEnumIterator as _};
#[cfg(any(feature = "wayland", feature = "x11"))]
use xkbcommon::xkb::{Keycode, Keymap, Keysym, MOD_NAME_SHIFT, State};
use crate::{PlatformKeyboardLayout, SharedString};
#[derive(Clone)]
#[derive(Debug, Clone)]
pub(crate) struct LinuxKeyboardLayout {
name: SharedString,
}
@@ -20,3 +27,449 @@ impl LinuxKeyboardLayout {
Self { name }
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) struct LinuxKeyboardMapper {
letters: HashMap<Keycode, String>,
code_to_key: HashMap<Keycode, String>,
code_to_shifted_key: HashMap<Keycode, String>,
}
#[cfg(any(feature = "wayland", feature = "x11"))]
impl LinuxKeyboardMapper {
pub(crate) fn new(
keymap: &Keymap,
base_group: u32,
latched_group: u32,
locked_group: u32,
) -> Self {
let mut xkb_state = State::new(keymap);
xkb_state.update_mask(0, 0, 0, base_group, latched_group, locked_group);
let mut shifted_state = State::new(&keymap);
let shift_mod = keymap.mod_get_index(MOD_NAME_SHIFT);
let shift_mask = 1 << shift_mod;
shifted_state.update_mask(shift_mask, 0, 0, base_group, latched_group, locked_group);
let mut letters = HashMap::default();
let mut code_to_key = HashMap::default();
let mut code_to_shifted_key = HashMap::default();
let mut inserted_letters = HashSet::default();
for scan_code in LinuxScanCodes::iter() {
let keycode = Keycode::new(scan_code as u32);
let key = xkb_state.key_get_utf8(keycode);
if !key.is_empty() {
if key_is_a_letter(&key) {
letters.insert(keycode, key.clone());
inserted_letters.insert(key);
} else {
code_to_key.insert(keycode, key.clone());
}
} else {
// keycode might be a dead key
let keysym = xkb_state.key_get_one_sym(keycode);
if let Some(key) = underlying_dead_key(keysym) {
code_to_key.insert(keycode, key.clone());
}
}
let shifted_key = shifted_state.key_get_utf8(keycode);
if !shifted_key.is_empty() {
code_to_shifted_key.insert(keycode, shifted_key);
} else {
// keycode might be a dead key
let shifted_keysym = shifted_state.key_get_one_sym(keycode);
if let Some(shifted_key) = underlying_dead_key(shifted_keysym) {
code_to_shifted_key.insert(keycode, shifted_key);
}
}
}
insert_letters_if_missing(&inserted_letters, &mut letters);
Self {
letters,
code_to_key,
code_to_shifted_key,
}
}
pub(crate) fn get_key(
&self,
keycode: Keycode,
modifiers: &mut crate::Modifiers,
) -> Option<String> {
if let Some(key) = self.letters.get(&keycode) {
return Some(key.clone());
}
if modifiers.shift {
modifiers.shift = false;
self.code_to_shifted_key.get(&keycode).cloned()
} else {
self.code_to_key.get(&keycode).cloned()
}
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
fn key_is_a_letter(key: &str) -> bool {
matches!(
key,
"a" | "b"
| "c"
| "d"
| "e"
| "f"
| "g"
| "h"
| "i"
| "j"
| "k"
| "l"
| "m"
| "n"
| "o"
| "p"
| "q"
| "r"
| "s"
| "t"
| "u"
| "v"
| "w"
| "x"
| "y"
| "z"
)
}
/**
* Returns which symbol the dead key represents
* <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
*/
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) fn underlying_dead_key(keysym: Keysym) -> Option<String> {
match keysym {
Keysym::dead_grave => Some("`".to_owned()),
Keysym::dead_acute => Some("´".to_owned()),
Keysym::dead_circumflex => Some("^".to_owned()),
Keysym::dead_tilde => Some("~".to_owned()),
Keysym::dead_macron => Some("¯".to_owned()),
Keysym::dead_breve => Some("˘".to_owned()),
Keysym::dead_abovedot => Some("˙".to_owned()),
Keysym::dead_diaeresis => Some("¨".to_owned()),
Keysym::dead_abovering => Some("˚".to_owned()),
Keysym::dead_doubleacute => Some("˝".to_owned()),
Keysym::dead_caron => Some("ˇ".to_owned()),
Keysym::dead_cedilla => Some("¸".to_owned()),
Keysym::dead_ogonek => Some("˛".to_owned()),
Keysym::dead_iota => Some("ͅ".to_owned()),
Keysym::dead_voiced_sound => Some("".to_owned()),
Keysym::dead_semivoiced_sound => Some("".to_owned()),
Keysym::dead_belowdot => Some("̣̣".to_owned()),
Keysym::dead_hook => Some("̡".to_owned()),
Keysym::dead_horn => Some("̛".to_owned()),
Keysym::dead_stroke => Some("̶̶".to_owned()),
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
Keysym::dead_doublegrave => Some("̏".to_owned()),
Keysym::dead_belowring => Some("˳".to_owned()),
Keysym::dead_belowmacron => Some("̱".to_owned()),
Keysym::dead_belowcircumflex => Some("".to_owned()),
Keysym::dead_belowtilde => Some("̰".to_owned()),
Keysym::dead_belowbreve => Some("̮".to_owned()),
Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
Keysym::dead_invertedbreve => Some("̯".to_owned()),
Keysym::dead_belowcomma => Some("̦".to_owned()),
Keysym::dead_currency => None,
Keysym::dead_lowline => None,
Keysym::dead_aboveverticalline => None,
Keysym::dead_belowverticalline => None,
Keysym::dead_longsolidusoverlay => None,
Keysym::dead_a => None,
Keysym::dead_A => None,
Keysym::dead_e => None,
Keysym::dead_E => None,
Keysym::dead_i => None,
Keysym::dead_I => None,
Keysym::dead_o => None,
Keysym::dead_O => None,
Keysym::dead_u => None,
Keysym::dead_U => None,
Keysym::dead_small_schwa => Some("ə".to_owned()),
Keysym::dead_capital_schwa => Some("Ə".to_owned()),
Keysym::dead_greek => None,
_ => None,
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
fn insert_letters_if_missing(inserted: &HashSet<String>, letters: &mut HashMap<Keycode, String>) {
for scan_code in LinuxScanCodes::LETTERS.iter() {
let keycode = Keycode::new(*scan_code as u32);
let key = scan_code.to_str();
if !inserted.contains(key) {
letters.insert(keycode, key.to_owned());
}
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
enum LinuxScanCodes {
A = 0x0026,
B = 0x0038,
C = 0x0036,
D = 0x0028,
E = 0x001a,
F = 0x0029,
G = 0x002a,
H = 0x002b,
I = 0x001f,
J = 0x002c,
K = 0x002d,
L = 0x002e,
M = 0x003a,
N = 0x0039,
O = 0x0020,
P = 0x0021,
Q = 0x0018,
R = 0x001b,
S = 0x0027,
T = 0x001c,
U = 0x001e,
V = 0x0037,
W = 0x0019,
X = 0x0035,
Y = 0x001d,
Z = 0x0034,
Digit0 = 0x0013,
Digit1 = 0x000a,
Digit2 = 0x000b,
Digit3 = 0x000c,
Digit4 = 0x000d,
Digit5 = 0x000e,
Digit6 = 0x000f,
Digit7 = 0x0010,
Digit8 = 0x0011,
Digit9 = 0x0012,
Backquote = 0x0031,
Minus = 0x0014,
Equal = 0x0015,
LeftBracket = 0x0022,
RightBracket = 0x0023,
Backslash = 0x0033,
Semicolon = 0x002f,
Quote = 0x0030,
Comma = 0x003b,
Period = 0x003c,
Slash = 0x003d,
// This key is typically located near LeftShift key, varies on international keyboards: Dan: <> Dutch: ][ Ger: <> UK: \|
IntlBackslash = 0x005e,
// Used for Brazilian /? and Japanese _ 'ro'.
IntlRo = 0x0061,
}
#[cfg(any(feature = "wayland", feature = "x11"))]
impl LinuxScanCodes {
const LETTERS: &'static [LinuxScanCodes] = &[
LinuxScanCodes::A,
LinuxScanCodes::B,
LinuxScanCodes::C,
LinuxScanCodes::D,
LinuxScanCodes::E,
LinuxScanCodes::F,
LinuxScanCodes::G,
LinuxScanCodes::H,
LinuxScanCodes::I,
LinuxScanCodes::J,
LinuxScanCodes::K,
LinuxScanCodes::L,
LinuxScanCodes::M,
LinuxScanCodes::N,
LinuxScanCodes::O,
LinuxScanCodes::P,
LinuxScanCodes::Q,
LinuxScanCodes::R,
LinuxScanCodes::S,
LinuxScanCodes::T,
LinuxScanCodes::U,
LinuxScanCodes::V,
LinuxScanCodes::W,
LinuxScanCodes::X,
LinuxScanCodes::Y,
LinuxScanCodes::Z,
];
fn to_str(&self) -> &str {
match self {
LinuxScanCodes::A => "a",
LinuxScanCodes::B => "b",
LinuxScanCodes::C => "c",
LinuxScanCodes::D => "d",
LinuxScanCodes::E => "e",
LinuxScanCodes::F => "f",
LinuxScanCodes::G => "g",
LinuxScanCodes::H => "h",
LinuxScanCodes::I => "i",
LinuxScanCodes::J => "j",
LinuxScanCodes::K => "k",
LinuxScanCodes::L => "l",
LinuxScanCodes::M => "m",
LinuxScanCodes::N => "n",
LinuxScanCodes::O => "o",
LinuxScanCodes::P => "p",
LinuxScanCodes::Q => "q",
LinuxScanCodes::R => "r",
LinuxScanCodes::S => "s",
LinuxScanCodes::T => "t",
LinuxScanCodes::U => "u",
LinuxScanCodes::V => "v",
LinuxScanCodes::W => "w",
LinuxScanCodes::X => "x",
LinuxScanCodes::Y => "y",
LinuxScanCodes::Z => "z",
LinuxScanCodes::Digit0 => "0",
LinuxScanCodes::Digit1 => "1",
LinuxScanCodes::Digit2 => "2",
LinuxScanCodes::Digit3 => "3",
LinuxScanCodes::Digit4 => "4",
LinuxScanCodes::Digit5 => "5",
LinuxScanCodes::Digit6 => "6",
LinuxScanCodes::Digit7 => "7",
LinuxScanCodes::Digit8 => "8",
LinuxScanCodes::Digit9 => "9",
LinuxScanCodes::Backquote => "`",
LinuxScanCodes::Minus => "-",
LinuxScanCodes::Equal => "=",
LinuxScanCodes::LeftBracket => "[",
LinuxScanCodes::RightBracket => "]",
LinuxScanCodes::Backslash => "\\",
LinuxScanCodes::Semicolon => ";",
LinuxScanCodes::Quote => "'",
LinuxScanCodes::Comma => ",",
LinuxScanCodes::Period => ".",
LinuxScanCodes::Slash => "/",
LinuxScanCodes::IntlBackslash => "unknown",
LinuxScanCodes::IntlRo => "unknown",
}
}
#[cfg(test)]
fn to_shifted(&self) -> &str {
match self {
LinuxScanCodes::A => "a",
LinuxScanCodes::B => "b",
LinuxScanCodes::C => "c",
LinuxScanCodes::D => "d",
LinuxScanCodes::E => "e",
LinuxScanCodes::F => "f",
LinuxScanCodes::G => "g",
LinuxScanCodes::H => "h",
LinuxScanCodes::I => "i",
LinuxScanCodes::J => "j",
LinuxScanCodes::K => "k",
LinuxScanCodes::L => "l",
LinuxScanCodes::M => "m",
LinuxScanCodes::N => "n",
LinuxScanCodes::O => "o",
LinuxScanCodes::P => "p",
LinuxScanCodes::Q => "q",
LinuxScanCodes::R => "r",
LinuxScanCodes::S => "s",
LinuxScanCodes::T => "t",
LinuxScanCodes::U => "u",
LinuxScanCodes::V => "v",
LinuxScanCodes::W => "w",
LinuxScanCodes::X => "x",
LinuxScanCodes::Y => "y",
LinuxScanCodes::Z => "z",
LinuxScanCodes::Digit0 => ")",
LinuxScanCodes::Digit1 => "!",
LinuxScanCodes::Digit2 => "@",
LinuxScanCodes::Digit3 => "#",
LinuxScanCodes::Digit4 => "$",
LinuxScanCodes::Digit5 => "%",
LinuxScanCodes::Digit6 => "^",
LinuxScanCodes::Digit7 => "&",
LinuxScanCodes::Digit8 => "*",
LinuxScanCodes::Digit9 => "(",
LinuxScanCodes::Backquote => "~",
LinuxScanCodes::Minus => "_",
LinuxScanCodes::Equal => "+",
LinuxScanCodes::LeftBracket => "{",
LinuxScanCodes::RightBracket => "}",
LinuxScanCodes::Backslash => "|",
LinuxScanCodes::Semicolon => ":",
LinuxScanCodes::Quote => "\"",
LinuxScanCodes::Comma => "<",
LinuxScanCodes::Period => ">",
LinuxScanCodes::Slash => "?",
LinuxScanCodes::IntlBackslash => "unknown",
LinuxScanCodes::IntlRo => "unknown",
}
}
}
#[cfg(all(test, any(feature = "wayland", feature = "x11")))]
mod tests {
use std::sync::LazyLock;
use strum::IntoEnumIterator;
use x11rb::{protocol::xkb::ConnectionExt, xcb_ffi::XCBConnection};
use xkbcommon::xkb::{
CONTEXT_NO_FLAGS, KEYMAP_COMPILE_NO_FLAGS, Keymap,
x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION},
};
use crate::platform::linux::keyboard::LinuxScanCodes;
use super::LinuxKeyboardMapper;
fn get_keymap() -> Keymap {
static XCB_CONNECTION: LazyLock<XCBConnection> =
LazyLock::new(|| XCBConnection::connect(None).unwrap().0);
let _ = XCB_CONNECTION
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
.unwrap()
.reply()
.unwrap();
let xkb_context = xkbcommon::xkb::Context::new(CONTEXT_NO_FLAGS);
let xkb_device_id = xkbcommon::xkb::x11::get_core_keyboard_device_id(&*XCB_CONNECTION);
xkbcommon::xkb::x11::keymap_new_from_device(
&xkb_context,
&*XCB_CONNECTION,
xkb_device_id,
KEYMAP_COMPILE_NO_FLAGS,
)
}
#[test]
fn test_us_layout_mapper() {
let keymap = get_keymap();
let mapper = LinuxKeyboardMapper::new(&keymap, 0, 0, 0);
for scan_code in super::LinuxScanCodes::iter() {
if scan_code == LinuxScanCodes::IntlBackslash || scan_code == LinuxScanCodes::IntlRo {
continue;
}
let keycode = xkbcommon::xkb::Keycode::new(scan_code as u32);
let key = mapper
.get_key(keycode, &mut crate::Modifiers::default())
.unwrap();
assert_eq!(key.as_str(), scan_code.to_str());
let shifted_key = mapper
.get_key(
keycode,
&mut crate::Modifiers {
shift: true,
..Default::default()
},
)
.unwrap();
assert_eq!(shifted_key.as_str(), scan_code.to_shifted());
}
}
}

View File

@@ -29,6 +29,9 @@ use crate::{
Point, Result, Task, WindowAppearance, WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
use super::LinuxKeyboardMapper;
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) const SCROLL_LINES: f32 = 3.0;
@@ -710,6 +713,7 @@ pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
impl crate::Keystroke {
pub(super) fn from_xkb(
state: &State,
keyboard_mapper: &LinuxKeyboardMapper,
mut modifiers: crate::Modifiers,
keycode: Keycode,
) -> Self {
@@ -718,76 +722,67 @@ impl crate::Keystroke {
let key_sym = state.key_get_one_sym(keycode);
let key = match key_sym {
Keysym::space => "space".to_owned(),
Keysym::BackSpace => "backspace".to_owned(),
Keysym::Return => "enter".to_owned(),
Keysym::Prior => "pageup".to_owned(),
Keysym::Next => "pagedown".to_owned(),
// Keysym::Tab => "tab".to_owned(),
Keysym::ISO_Left_Tab => "tab".to_owned(),
Keysym::KP_Prior => "pageup".to_owned(),
Keysym::KP_Next => "pagedown".to_owned(),
Keysym::uparrow => "up".to_owned(),
Keysym::downarrow => "down".to_owned(),
Keysym::leftarrow => "left".to_owned(),
Keysym::rightarrow => "right".to_owned(),
Keysym::Home | Keysym::KP_Home => "home".to_owned(),
Keysym::End | Keysym::KP_End => "end".to_owned(),
Keysym::Prior | Keysym::KP_Prior => "pageup".to_owned(),
Keysym::Next | Keysym::KP_Next => "pagedown".to_owned(),
Keysym::XF86_Back => "back".to_owned(),
Keysym::XF86_Forward => "forward".to_owned(),
Keysym::Escape => "escape".to_owned(),
Keysym::Insert | Keysym::KP_Insert => "insert".to_owned(),
Keysym::Delete | Keysym::KP_Delete => "delete".to_owned(),
Keysym::Menu => "menu".to_owned(),
Keysym::XF86_Cut => "cut".to_owned(),
Keysym::XF86_Copy => "copy".to_owned(),
Keysym::XF86_Paste => "paste".to_owned(),
Keysym::XF86_New => "new".to_owned(),
Keysym::XF86_Open => "open".to_owned(),
Keysym::XF86_Save => "save".to_owned(),
Keysym::comma => ",".to_owned(),
Keysym::period => ".".to_owned(),
Keysym::less => "<".to_owned(),
Keysym::greater => ">".to_owned(),
Keysym::slash => "/".to_owned(),
Keysym::question => "?".to_owned(),
Keysym::semicolon => ";".to_owned(),
Keysym::colon => ":".to_owned(),
Keysym::apostrophe => "'".to_owned(),
Keysym::quotedbl => "\"".to_owned(),
Keysym::bracketleft => "[".to_owned(),
Keysym::braceleft => "{".to_owned(),
Keysym::bracketright => "]".to_owned(),
Keysym::braceright => "}".to_owned(),
Keysym::backslash => "\\".to_owned(),
Keysym::bar => "|".to_owned(),
Keysym::grave => "`".to_owned(),
Keysym::asciitilde => "~".to_owned(),
Keysym::exclam => "!".to_owned(),
Keysym::at => "@".to_owned(),
Keysym::numbersign => "#".to_owned(),
Keysym::dollar => "$".to_owned(),
Keysym::percent => "%".to_owned(),
Keysym::asciicircum => "^".to_owned(),
Keysym::ampersand => "&".to_owned(),
Keysym::asterisk => "*".to_owned(),
Keysym::parenleft => "(".to_owned(),
Keysym::parenright => ")".to_owned(),
Keysym::minus => "-".to_owned(),
Keysym::underscore => "_".to_owned(),
Keysym::equal => "=".to_owned(),
Keysym::plus => "+".to_owned(),
_ => {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
if key_sym.is_keypad_key() {
name.replace("kp_", "")
} else {
name
}
}
Keysym::F1 => "f1".to_owned(),
Keysym::F2 => "f2".to_owned(),
Keysym::F3 => "f3".to_owned(),
Keysym::F4 => "f4".to_owned(),
Keysym::F5 => "f5".to_owned(),
Keysym::F6 => "f6".to_owned(),
Keysym::F7 => "f7".to_owned(),
Keysym::F8 => "f8".to_owned(),
Keysym::F9 => "f9".to_owned(),
Keysym::F10 => "f10".to_owned(),
Keysym::F11 => "f11".to_owned(),
Keysym::F12 => "f12".to_owned(),
Keysym::F13 => "f13".to_owned(),
Keysym::F14 => "f14".to_owned(),
Keysym::F15 => "f15".to_owned(),
Keysym::F16 => "f16".to_owned(),
Keysym::F17 => "f17".to_owned(),
Keysym::F18 => "f18".to_owned(),
Keysym::F19 => "f19".to_owned(),
Keysym::F20 => "f20".to_owned(),
Keysym::F21 => "f21".to_owned(),
Keysym::F22 => "f22".to_owned(),
Keysym::F23 => "f23".to_owned(),
Keysym::F24 => "f24".to_owned(),
_ => keyboard_mapper
.get_key(keycode, &mut modifiers)
.unwrap_or_else(|| {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
if key_sym.is_keypad_key() {
name.replace("kp_", "")
} else {
name
}
}),
};
if modifiers.shift {
// we only include the shift for upper-case letters by convention,
// so don't include for numbers and symbols, but do include for
// tab/enter, etc.
if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
modifiers.shift = false;
}
}
// Ignore control characters (and DEL) for the purposes of key_char
let key_char =
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
@@ -798,65 +793,6 @@ impl crate::Keystroke {
key_char,
}
}
/**
* Returns which symbol the dead key represents
* <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
*/
pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
match keysym {
Keysym::dead_grave => Some("`".to_owned()),
Keysym::dead_acute => Some("´".to_owned()),
Keysym::dead_circumflex => Some("^".to_owned()),
Keysym::dead_tilde => Some("~".to_owned()),
Keysym::dead_macron => Some("¯".to_owned()),
Keysym::dead_breve => Some("˘".to_owned()),
Keysym::dead_abovedot => Some("˙".to_owned()),
Keysym::dead_diaeresis => Some("¨".to_owned()),
Keysym::dead_abovering => Some("˚".to_owned()),
Keysym::dead_doubleacute => Some("˝".to_owned()),
Keysym::dead_caron => Some("ˇ".to_owned()),
Keysym::dead_cedilla => Some("¸".to_owned()),
Keysym::dead_ogonek => Some("˛".to_owned()),
Keysym::dead_iota => Some("ͅ".to_owned()),
Keysym::dead_voiced_sound => Some("".to_owned()),
Keysym::dead_semivoiced_sound => Some("".to_owned()),
Keysym::dead_belowdot => Some("̣̣".to_owned()),
Keysym::dead_hook => Some("̡".to_owned()),
Keysym::dead_horn => Some("̛".to_owned()),
Keysym::dead_stroke => Some("̶̶".to_owned()),
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
Keysym::dead_doublegrave => Some("̏".to_owned()),
Keysym::dead_belowring => Some("˳".to_owned()),
Keysym::dead_belowmacron => Some("̱".to_owned()),
Keysym::dead_belowcircumflex => Some("".to_owned()),
Keysym::dead_belowtilde => Some("̰".to_owned()),
Keysym::dead_belowbreve => Some("̮".to_owned()),
Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
Keysym::dead_invertedbreve => Some("̯".to_owned()),
Keysym::dead_belowcomma => Some("̦".to_owned()),
Keysym::dead_currency => None,
Keysym::dead_lowline => None,
Keysym::dead_aboveverticalline => None,
Keysym::dead_belowverticalline => None,
Keysym::dead_longsolidusoverlay => None,
Keysym::dead_a => None,
Keysym::dead_A => None,
Keysym::dead_e => None,
Keysym::dead_E => None,
Keysym::dead_i => None,
Keysym::dead_I => None,
Keysym::dead_o => None,
Keysym::dead_O => None,
Keysym::dead_u => None,
Keysym::dead_U => None,
Keysym::dead_small_schwa => Some("ə".to_owned()),
Keysym::dead_capital_schwa => Some("Ə".to_owned()),
Keysym::dead_greek => None,
_ => None,
}
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]

View File

@@ -61,22 +61,20 @@ use wayland_protocols::xdg::decoration::zv1::client::{
};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, ffi::XKB_KEYMAP_FORMAT_TEXT_V1};
use super::{
display::WaylandDisplay,
window::{ImeInput, WaylandWindowStatePtr},
};
use crate::platform::{PlatformWindow, blade::BladeContext};
use crate::{
AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScrollDelta,
ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
LinuxKeyboardLayout, LinuxKeyboardMapper, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels,
ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
};
use crate::{
SharedString,
@@ -92,6 +90,10 @@ use crate::{
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
},
};
use crate::{
platform::{PlatformWindow, blade::BladeContext},
underlying_dead_key,
};
/// Used to convert evdev scancode to xkb scancode
const MIN_KEYCODE: u32 = 8;
@@ -208,9 +210,11 @@ pub(crate) struct WaylandClientState {
// Output to scale mapping
outputs: HashMap<ObjectId, Output>,
in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
keyboard_layout: LinuxKeyboardLayout,
keymap_state: Option<xkb::State>,
compose_state: Option<xkb::compose::State>,
keyboard_layout: LinuxKeyboardLayout,
keyboard_mapper: Option<Rc<LinuxKeyboardMapper>>,
keyboard_mapper_cache: HashMap<String, Rc<LinuxKeyboardMapper>>,
drag: DragState,
click: ClickState,
repeat: KeyRepeat,
@@ -340,7 +344,7 @@ impl WaylandClientStatePtr {
text_input.commit();
}
pub fn handle_keyboard_layout_change(&self) {
pub fn handle_keyboard_layout_change(&self, locked_group: u32) {
let client = self.get_client();
let mut state = client.borrow_mut();
let changed = if let Some(keymap_state) = &state.keymap_state {
@@ -350,6 +354,17 @@ impl WaylandClientStatePtr {
let changed = layout_name != state.keyboard_layout.name();
if changed {
state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into());
let mapper = state
.keyboard_mapper_cache
.entry(layout_name.to_string())
.or_insert(Rc::new(LinuxKeyboardMapper::new(
&keymap,
0,
0,
locked_group,
)))
.clone();
state.keyboard_mapper = Some(mapper);
}
changed
} else {
@@ -447,6 +462,7 @@ impl WaylandClient {
pub(crate) fn new() -> Self {
let conn = Connection::connect_to_env().unwrap();
let keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME);
let (globals, mut event_queue) =
registry_queue_init::<WaylandClientStatePtr>(&conn).unwrap();
let qh = event_queue.handle();
@@ -567,9 +583,11 @@ impl WaylandClient {
in_progress_outputs,
windows: HashMap::default(),
common,
keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME),
keymap_state: None,
compose_state: None,
keyboard_layout,
keyboard_mapper: None,
keyboard_mapper_cache: HashMap::default(),
drag: DragState {
data_offer: None,
window: None,
@@ -1214,7 +1232,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
state.compose_state = get_xkb_compose_state(&xkb_context);
drop(state);
this.handle_keyboard_layout_change();
this.handle_keyboard_layout_change(0);
}
wl_keyboard::Event::Enter { surface, .. } => {
state.keyboard_focused_window = get_window(&mut state, &surface.id());
@@ -1270,7 +1288,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
}
if group != old_layout {
this.handle_keyboard_layout_change();
this.handle_keyboard_layout_change(group);
}
}
wl_keyboard::Event::Key {
@@ -1288,20 +1306,25 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
let focused_window = focused_window.clone();
let keymap_state = state.keymap_state.as_ref().unwrap();
let keycode = Keycode::from(key + MIN_KEYCODE);
let keyboard_mapper = state.keyboard_mapper.as_ref().unwrap();
let keycode = xkb::Keycode::from(key + MIN_KEYCODE);
let keysym = keymap_state.key_get_one_sym(keycode);
match key_state {
wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
let mut keystroke =
Keystroke::from_xkb(&keymap_state, state.modifiers, keycode);
let mut keystroke = Keystroke::from_xkb(
keymap_state,
keyboard_mapper,
state.modifiers,
keycode,
);
if let Some(mut compose) = state.compose_state.take() {
compose.feed(keysym);
match compose.status() {
xkb::Status::Composing => {
keystroke.key_char = None;
state.pre_edit_text =
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
compose.utf8().or(underlying_dead_key(keysym));
let pre_edit =
state.pre_edit_text.clone().unwrap_or(String::default());
drop(state);
@@ -1318,7 +1341,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
}
xkb::Status::Cancelled => {
let pre_edit = state.pre_edit_text.take();
let new_pre_edit = Keystroke::underlying_dead_key(keysym);
let new_pre_edit = underlying_dead_key(keysym);
state.pre_edit_text = new_pre_edit.clone();
drop(state);
if let Some(pre_edit) = pre_edit {
@@ -1379,7 +1402,12 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
}
wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => {
let input = PlatformInput::KeyUp(KeyUpEvent {
keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
keystroke: Keystroke::from_xkb(
keymap_state,
keyboard_mapper,
state.modifiers,
keycode,
),
});
if state.repeat.current_keycode == Some(keycode) {

View File

@@ -1,4 +1,3 @@
use crate::{Capslock, xcb_flush};
use core::str;
use std::{
cell::RefCell,
@@ -49,7 +48,7 @@ use super::{
};
use crate::platform::{
LinuxCommon, PlatformWindow,
Capslock, LinuxCommon, PlatformWindow,
blade::BladeContext,
linux::{
DEFAULT_CURSOR_ICON_NAME, LinuxClient, get_xkb_compose_state, is_within_click_distance,
@@ -58,13 +57,14 @@ use crate::platform::{
reveal_path_internal,
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
},
xcb_flush,
};
use crate::{
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform,
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions,
ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
modifiers_from_xinput_info, point, px,
LinuxKeyboardLayout, LinuxKeyboardMapper, Modifiers, ModifiersChangedEvent, MouseButton,
Pixels, Platform, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point,
RequestFrameOptions, ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
modifiers_from_xinput_info, point, px, underlying_dead_key,
};
/// Value for DeviceId parameters which selects all devices.
@@ -200,6 +200,8 @@ pub struct X11ClientState {
pub(crate) xkb: xkbc::State,
previous_xkb_state: XKBStateNotiy,
keyboard_layout: LinuxKeyboardLayout,
keyboard_mapper: Rc<LinuxKeyboardMapper>,
keyboard_mapper_cache: HashMap<String, Rc<LinuxKeyboardMapper>>,
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
pub(crate) xim_handler: Option<XimHandler>,
pub modifiers: Modifiers,
@@ -403,22 +405,24 @@ impl X11Client {
let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS);
let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection);
let xkb_state = {
let xkb_keymap = xkbc::x11::keymap_new_from_device(
&xkb_context,
&xcb_connection,
xkb_device_id,
xkbc::KEYMAP_COMPILE_NO_FLAGS,
);
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
};
let xkb_keymap = xkbc::x11::keymap_new_from_device(
&xkb_context,
&xcb_connection,
xkb_device_id,
xkbc::KEYMAP_COMPILE_NO_FLAGS,
);
let xkb_state =
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id);
let compose_state = get_xkb_compose_state(&xkb_context);
let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE);
let layout_name = xkb_state
.get_keymap()
.layout_get_name(layout_idx)
.to_string();
let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into());
let keyboard_layout = LinuxKeyboardLayout::new(layout_name.clone().into());
let keyboard_mapper = Rc::new(LinuxKeyboardMapper::new(&xkb_keymap, 0, 0, 0));
let mut keyboard_mapper_cache = HashMap::default();
keyboard_mapper_cache.insert(layout_name, keyboard_mapper.clone());
let gpu_context = BladeContext::new().context("Unable to init GPU context")?;
@@ -512,6 +516,8 @@ impl X11Client {
xkb: xkb_state,
previous_xkb_state: XKBStateNotiy::default(),
keyboard_layout,
keyboard_mapper,
keyboard_mapper_cache,
ximc,
xim_handler,
@@ -972,24 +978,27 @@ impl X11Client {
};
state.xkb = xkb_state;
drop(state);
self.handle_keyboard_layout_change();
self.handle_keyboard_layout_change(depressed_layout, latched_layout, locked_layout);
}
Event::XkbStateNotify(event) => {
let mut state = self.0.borrow_mut();
let old_layout = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
let new_layout = u32::from(event.group);
let base_group = event.base_group as u32;
let latched_group = event.latched_group as u32;
let locked_group = event.locked_group.into();
state.xkb.update_mask(
event.base_mods.into(),
event.latched_mods.into(),
event.locked_mods.into(),
event.base_group as u32,
event.latched_group as u32,
event.locked_group.into(),
base_group,
latched_group,
locked_group,
);
state.previous_xkb_state = XKBStateNotiy {
depressed_layout: event.base_group as u32,
latched_layout: event.latched_group as u32,
locked_layout: event.locked_group.into(),
depressed_layout: base_group,
latched_layout: latched_group,
locked_layout: locked_group,
};
let modifiers = Modifiers::from_xkb(&state.xkb);
@@ -1016,7 +1025,7 @@ impl X11Client {
}
if new_layout != old_layout {
self.handle_keyboard_layout_change();
self.handle_keyboard_layout_change(base_group, latched_group, locked_group);
}
}
Event::KeyPress(event) => {
@@ -1037,7 +1046,12 @@ impl X11Client {
xkb_state.latched_layout,
xkb_state.locked_layout,
);
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
let mut keystroke = crate::Keystroke::from_xkb(
&state.xkb,
&state.keyboard_mapper,
modifiers,
code,
);
let keysym = state.xkb.key_get_one_sym(code);
if keysym.is_modifier_key() {
return Some(());
@@ -1054,9 +1068,8 @@ impl X11Client {
}
xkbc::Status::Composing => {
keystroke.key_char = None;
state.pre_edit_text = compose_state
.utf8()
.or(crate::Keystroke::underlying_dead_key(keysym));
state.pre_edit_text =
compose_state.utf8().or(underlying_dead_key(keysym));
let pre_edit =
state.pre_edit_text.clone().unwrap_or(String::default());
drop(state);
@@ -1069,7 +1082,7 @@ impl X11Client {
if let Some(pre_edit) = pre_edit {
window.handle_ime_commit(pre_edit);
}
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
if let Some(current_key) = underlying_dead_key(keysym) {
window.handle_ime_preedit(current_key);
}
state = self.0.borrow_mut();
@@ -1105,7 +1118,12 @@ impl X11Client {
xkb_state.latched_layout,
xkb_state.locked_layout,
);
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
let keystroke = crate::Keystroke::from_xkb(
&state.xkb,
&state.keyboard_mapper,
modifiers,
code,
);
let keysym = state.xkb.key_get_one_sym(code);
if keysym.is_modifier_key() {
return Some(());
@@ -1327,6 +1345,7 @@ impl X11Client {
let mut state = self.0.borrow_mut();
state.pre_key_char_down = Some(Keystroke::from_xkb(
&state.xkb,
&state.keyboard_mapper,
state.modifiers,
event.detail.into(),
));
@@ -1412,13 +1431,29 @@ impl X11Client {
Some(())
}
fn handle_keyboard_layout_change(&self) {
fn handle_keyboard_layout_change(
&self,
base_group: u32,
latched_group: u32,
locked_group: u32,
) {
let mut state = self.0.borrow_mut();
let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
let keymap = state.xkb.get_keymap();
let layout_name = keymap.layout_get_name(layout_idx);
if layout_name != state.keyboard_layout.name() {
state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into());
let mapper = state
.keyboard_mapper_cache
.entry(layout_name.to_string())
.or_insert(Rc::new(LinuxKeyboardMapper::new(
&keymap,
base_group,
latched_group,
locked_group,
)))
.clone();
state.keyboard_mapper = mapper;
if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
drop(state);
callback();

View File

@@ -7,7 +7,7 @@ use super::{
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
MacDisplay, MacWindow, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
WindowAppearance, WindowParams, hash,
};
@@ -170,6 +170,7 @@ pub(crate) struct MacPlatformState {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
finish_launching: Option<Box<dyn FnOnce()>>,
dock_menu: Option<id>,
menus: Option<Vec<OwnedMenu>>,
}
impl Default for MacPlatform {
@@ -207,6 +208,7 @@ impl MacPlatform {
finish_launching: None,
dock_menu: None,
on_keyboard_layout_change: None,
menus: None,
}))
}
@@ -226,7 +228,7 @@ impl MacPlatform {
unsafe fn create_menu_bar(
&self,
menus: Vec<Menu>,
menus: &Vec<Menu>,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keymap: &Keymap,
@@ -241,7 +243,7 @@ impl MacPlatform {
menu.setTitle_(menu_title);
menu.setDelegate_(delegate);
for item_config in menu_config.items {
for item_config in &menu_config.items {
menu.addItem_(Self::create_menu_item(
item_config,
delegate,
@@ -277,7 +279,7 @@ impl MacPlatform {
dock_menu.setDelegate_(delegate);
for item_config in menu_items {
dock_menu.addItem_(Self::create_menu_item(
item_config,
&item_config,
delegate,
actions,
keymap,
@@ -289,7 +291,7 @@ impl MacPlatform {
}
unsafe fn create_menu_item(
item: MenuItem,
item: &MenuItem,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keymap: &Keymap,
@@ -399,7 +401,7 @@ impl MacPlatform {
let tag = actions.len() as NSInteger;
let _: () = msg_send![item, setTag: tag];
actions.push(action);
actions.push(action.boxed_clone());
item
}
MenuItem::Submenu(Menu { name, items }) => {
@@ -865,10 +867,15 @@ impl Platform for MacPlatform {
let app: id = msg_send![APP_CLASS, sharedApplication];
let mut state = self.0.lock();
let actions = &mut state.menu_actions;
let menu = self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap);
let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap);
drop(state);
app.setMainMenu_(menu);
}
self.0.lock().menus = Some(menus.into_iter().map(|menu| menu.owned()).collect());
}
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
self.0.lock().menus.clone()
}
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {

View File

@@ -1248,7 +1248,9 @@ fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) ->
}
_ => {
lock.pending_surrogate = None;
String::from_utf16(&[code_point]).ok()
char::from_u32(code_point as u32)
.filter(|c| !c.is_control())
.map(|c| c.to_string())
}
}
}

View File

@@ -1369,6 +1369,31 @@ impl Window {
});
}
pub(crate) fn dispatch_keystroke_interceptors(
&mut self,
event: &dyn Any,
context_stack: Vec<KeyContext>,
cx: &mut App,
) {
let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() else {
return;
};
cx.keystroke_interceptors
.clone()
.retain(&(), move |callback| {
(callback)(
&KeystrokeEvent {
keystroke: key_down_event.keystroke.clone(),
action: None,
context_stack: context_stack.clone(),
},
self,
cx,
)
});
}
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
/// that are currently on the stack to be returned to the app.
pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) {
@@ -3522,6 +3547,13 @@ impl Window {
return;
};
cx.propagate_event = true;
self.dispatch_keystroke_interceptors(event, self.context_stack(), cx);
if !cx.propagate_event {
self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx);
return;
}
let mut currently_pending = self.pending_input.take().unwrap_or_default();
if currently_pending.focus.is_some() && currently_pending.focus != self.focus {
currently_pending = PendingInput::default();
@@ -3570,7 +3602,6 @@ impl Window {
return;
}
cx.propagate_event = true;
for binding in match_result.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
if !cx.propagate_event {

View File

@@ -13,6 +13,7 @@ pub enum IconName {
AiBedrock,
AiDeepSeek,
AiEdit,
AiGemini,
AiGoogle,
AiLmStudio,
AiMistral,
@@ -65,7 +66,6 @@ pub enum IconName {
Circle,
CircleOff,
CircleHelp,
Clipboard,
Close,
Cloud,
Code,
@@ -117,7 +117,6 @@ pub enum IconName {
File,
FileCode,
FileCreate,
FileDelete,
FileDiff,
FileDoc,
FileGeneric,
@@ -214,7 +213,6 @@ pub enum IconName {
Scissors,
Screen,
ScrollText,
SearchCode,
SearchSelection,
SelectAll,
Send,
@@ -252,6 +250,19 @@ pub enum IconName {
TextSnippet,
ThumbsDown,
ThumbsUp,
ToolBulb,
ToolCopy,
ToolDeleteFile,
ToolDiagnostics,
ToolFolder,
ToolHammer,
ToolNotification,
ToolPencil,
ToolRead,
ToolRegex,
ToolSearch,
ToolTerminal,
ToolWeb,
Trash,
TrashAlt,
Triangle,

View File

@@ -391,6 +391,7 @@ pub struct LanguageModelRequest {
pub tool_choice: Option<LanguageModelToolChoice>,
pub stop: Vec<String>,
pub temperature: Option<f32>,
pub thinking_allowed: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]

View File

@@ -663,7 +663,9 @@ pub fn into_anthropic(
} else {
Some(anthropic::StringOrContents::String(system_message))
},
thinking: if let AnthropicModelMode::Thinking { budget_tokens } = mode {
thinking: if request.thinking_allowed
&& let AnthropicModelMode::Thinking { budget_tokens } = mode
{
Some(anthropic::Thinking::Enabled { budget_tokens })
} else {
None
@@ -1108,6 +1110,7 @@ mod tests {
temperature: None,
tools: vec![],
tool_choice: None,
thinking_allowed: true,
};
let anthropic_request = into_anthropic(

View File

@@ -799,7 +799,9 @@ pub fn into_bedrock(
max_tokens: max_output_tokens,
system: Some(system_message),
tools: Some(tool_config),
thinking: if let BedrockModelMode::Thinking { budget_tokens } = mode {
thinking: if request.thinking_allowed
&& let BedrockModelMode::Thinking { budget_tokens } = mode
{
Some(bedrock::Thinking::Enabled { budget_tokens })
} else {
None

View File

@@ -849,6 +849,7 @@ impl LanguageModel for CloudLanguageModel {
let use_cloud = cx
.update(|cx| cx.has_flag::<ZedCloudFeatureFlag>())
.unwrap_or(false);
let thinking_allowed = request.thinking_allowed;
match self.model.provider {
zed_llm_client::LanguageModelProvider::Anthropic => {
let request = into_anthropic(
@@ -856,7 +857,7 @@ impl LanguageModel for CloudLanguageModel {
self.model.id.to_string(),
1.0,
self.model.max_output_tokens as u64,
if self.model.id.0.ends_with("-thinking") {
if thinking_allowed && self.model.id.0.ends_with("-thinking") {
AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
}

View File

@@ -559,11 +559,11 @@ pub fn into_google(
stop_sequences: Some(request.stop),
max_output_tokens: None,
temperature: request.temperature.map(|t| t as f64).or(Some(1.0)),
thinking_config: match mode {
GoogleModelMode::Thinking { budget_tokens } => {
thinking_config: match (request.thinking_allowed, mode) {
(true, GoogleModelMode::Thinking { budget_tokens }) => {
budget_tokens.map(|thinking_budget| ThinkingConfig { thinking_budget })
}
GoogleModelMode::Default => None,
_ => None,
},
top_p: None,
top_k: None,

View File

@@ -911,6 +911,7 @@ mod tests {
intent: None,
mode: None,
stop: vec![],
thinking_allowed: true,
};
let mistral_request = into_mistral(request, "mistral-small-latest".into(), None);
@@ -943,6 +944,7 @@ mod tests {
intent: None,
mode: None,
stop: vec![],
thinking_allowed: true,
};
let mistral_request = into_mistral(request, "pixtral-12b-latest".into(), None);

View File

@@ -334,7 +334,10 @@ impl OllamaLanguageModel {
temperature: request.temperature.or(Some(1.0)),
..Default::default()
}),
think: self.model.supports_thinking,
think: self
.model
.supports_thinking
.map(|supports_thinking| supports_thinking && request.thinking_allowed),
tools: request.tools.into_iter().map(tool_into_ollama).collect(),
}
}

View File

@@ -999,6 +999,7 @@ mod tests {
tool_choice: None,
stop: vec![],
temperature: None,
thinking_allowed: true,
};
// Validate that all models are supported by tiktoken-rs

View File

@@ -523,7 +523,9 @@ pub fn into_open_router(
None
},
usage: open_router::RequestUsage { include: true },
reasoning: if let OpenRouterModelMode::Thinking { budget_tokens } = model.mode {
reasoning: if request.thinking_allowed
&& let OpenRouterModelMode::Thinking { budget_tokens } = model.mode
{
Some(open_router::Reasoning {
effort: None,
max_tokens: budget_tokens,

View File

@@ -1331,6 +1331,7 @@ impl Render for LspLogToolbarItemView {
let Some(log_view) = self.log_view.clone() else {
return div();
};
let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
let menu_rows = log_view.menu_items(cx).unwrap_or_default();
let current_server_id = log_view.current_server_id;
@@ -1344,6 +1345,7 @@ impl Render for LspLogToolbarItemView {
None
}
});
let available_language_servers: Vec<_> = menu_rows
.into_iter()
.map(|row| {
@@ -1355,21 +1357,28 @@ impl Render for LspLogToolbarItemView {
)
})
.collect();
let log_toolbar_view = cx.entity().clone();
let lsp_menu = PopoverMenu::new("LspLogView")
.anchor(Corner::TopLeft)
.trigger(Button::new(
"language_server_menu_header",
current_server
.as_ref()
.map(|row| {
Cow::Owned(format!(
"{} ({})",
row.server_name.0, row.worktree_root_name,
))
})
.unwrap_or_else(|| "No server selected".into()),
))
.trigger(
Button::new(
"language_server_menu_header",
current_server
.as_ref()
.map(|row| {
Cow::Owned(format!(
"{} ({})",
row.server_name.0, row.worktree_root_name,
))
})
.unwrap_or_else(|| "No server selected".into()),
)
.icon(IconName::ChevronDown)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu({
let log_view = log_view.clone();
move |window, cx| {
@@ -1413,6 +1422,7 @@ impl Render for LspLogToolbarItemView {
.into()
}
});
let view_selector = current_server.map(|server| {
let server_id = server.server_id;
let is_remote = server.server_kind.is_remote();
@@ -1420,10 +1430,12 @@ impl Render for LspLogToolbarItemView {
let log_view = log_view.clone();
PopoverMenu::new("LspViewSelector")
.anchor(Corner::TopLeft)
.trigger(Button::new(
"language_server_menu_header",
server.selected_entry.label(),
))
.trigger(
Button::new("language_server_menu_header", server.selected_entry.label())
.icon(IconName::ChevronDown)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu(move |window, cx| {
let log_toolbar_view = log_toolbar_view.clone();
let log_view = log_view.clone();
@@ -1494,11 +1506,14 @@ impl Render for LspLogToolbarItemView {
}))
})
});
h_flex()
.size_full()
.gap_1()
.justify_between()
.child(
h_flex()
.gap_0p5()
.child(lsp_menu)
.children(view_selector)
.child(
@@ -1508,10 +1523,15 @@ impl Render for LspLogToolbarItemView {
div().child(
PopoverMenu::new("lsp-trace-level-menu")
.anchor(Corner::TopLeft)
.trigger(Button::new(
"language_server_trace_level_selector",
"Trace level",
))
.trigger(
Button::new(
"language_server_trace_level_selector",
"Trace level",
)
.icon(IconName::ChevronDown)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu({
let log_view = log_view.clone();
@@ -1571,10 +1591,15 @@ impl Render for LspLogToolbarItemView {
div().child(
PopoverMenu::new("lsp-log-level-menu")
.anchor(Corner::TopLeft)
.trigger(Button::new(
"language_server_log_level_selector",
"Log level",
))
.trigger(
Button::new(
"language_server_log_level_selector",
"Log level",
)
.icon(IconName::ChevronDown)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu({
let log_view = log_view.clone();
@@ -1635,23 +1660,19 @@ impl Render for LspLogToolbarItemView {
),
)
.child(
div()
.child(
Button::new("clear_log_button", "Clear").on_click(cx.listener(
|this, _, window, cx| {
if let Some(log_view) = this.log_view.as_ref() {
log_view.update(cx, |log_view, cx| {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.clear(window, cx);
editor.set_read_only(true);
});
})
}
},
)),
)
.ml_2(),
Button::new("clear_log_button", "Clear").on_click(cx.listener(
|this, _, window, cx| {
if let Some(log_view) = this.log_view.as_ref() {
log_view.update(cx, |log_view, cx| {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.clear(window, cx);
editor.set_read_only(true);
});
})
}
},
)),
)
}
}

View File

@@ -352,6 +352,14 @@ pub fn debug_adapters_dir() -> &'static PathBuf {
DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters"))
}
/// Returns the path to the agent servers directory
///
/// This is where agent servers are downloaded to
pub fn agent_servers_dir() -> &'static PathBuf {
static AGENT_SERVERS_DIR: OnceLock<PathBuf> = OnceLock::new();
AGENT_SERVERS_DIR.get_or_init(|| data_dir().join("agent_servers"))
}
/// Returns the path to the Copilot directory.
pub fn copilot_dir() -> &'static PathBuf {
static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new();

View File

@@ -84,7 +84,7 @@ impl ProjectEnvironment {
self.get_worktree_environment(worktree, cx)
}
pub(crate) fn get_worktree_environment(
pub fn get_worktree_environment(
&mut self,
worktree: Entity<Worktree>,
cx: &mut Context<Self>,
@@ -118,7 +118,7 @@ impl ProjectEnvironment {
/// If the project was opened from the CLI, then the inherited CLI environment is returned.
/// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in
/// that directory, to get environment variables as if the user has `cd`'d there.
pub(crate) fn get_directory_environment(
pub fn get_directory_environment(
&mut self,
abs_path: Arc<Path>,
cx: &mut Context<Self>,

View File

@@ -10671,6 +10671,21 @@ impl LspStore {
}
// Tell the language server about every open buffer in the worktree that matches the language.
// Also check for buffers in worktrees that reused this server
let mut worktrees_using_server = vec![key.0];
if let Some(local) = self.as_local() {
// Find all worktrees that have this server in their language server tree
for (worktree_id, servers) in &local.lsp_tree.read(cx).instances {
if *worktree_id != key.0 {
for (_, server_map) in &servers.roots {
if server_map.contains_key(&key.1) {
worktrees_using_server.push(*worktree_id);
}
}
}
}
}
let mut buffer_paths_registered = Vec::new();
self.buffer_store.clone().update(cx, |buffer_store, cx| {
for buffer_handle in buffer_store.buffers() {
@@ -10684,7 +10699,7 @@ impl LspStore {
None => continue,
};
if file.worktree.read(cx).id() != key.0
if !worktrees_using_server.contains(&file.worktree.read(cx).id())
|| !self
.languages
.lsp_adapters(&language.name())

View File

@@ -981,6 +981,7 @@ impl RulesLibrary {
tool_choice: None,
stop: Vec::new(),
temperature: None,
thinking_allowed: true,
},
cx,
)

View File

@@ -570,6 +570,7 @@ impl SummaryIndex {
tool_choice: None,
stop: Vec::new(),
temperature: None,
thinking_allowed: true,
};
let code_len = code.len();

View File

@@ -12,7 +12,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AppContext as _, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription,
WeakEntity, actions, div, transparent_black,
WeakEntity, actions, div,
};
use language::{Language, LanguageConfig, ToOffset as _};
use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
@@ -31,6 +31,8 @@ use crate::{
ui_components::table::{Table, TableInteractionState},
};
const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
actions!(
zed,
[
@@ -45,6 +47,8 @@ actions!(
[
/// Edits the selected key binding.
EditBinding,
/// Creates a new key binding for the selected action.
CreateBinding,
/// Copies the action name to clipboard.
CopyAction,
/// Copies the context predicate to clipboard.
@@ -133,11 +137,29 @@ impl KeymapEventChannel {
}
}
#[derive(Default, PartialEq)]
enum FilterState {
#[default]
All,
Conflicts,
}
impl FilterState {
fn invert(&self) -> Self {
match self {
FilterState::All => FilterState::Conflicts,
FilterState::Conflicts => FilterState::All,
}
}
}
struct KeymapEditor {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
_keymap_subscription: Subscription,
keybindings: Vec<ProcessedKeybinding>,
conflicts: Vec<usize>,
filter_state: FilterState,
// corresponds 1 to 1 with keybindings
string_match_candidates: Arc<Vec<StringMatchCandidate>>,
matches: Vec<StringMatch>,
@@ -180,6 +202,8 @@ impl KeymapEditor {
let mut this = Self {
workspace,
keybindings: vec![],
conflicts: Vec::default(),
filter_state: FilterState::default(),
string_match_candidates: Arc::new(vec![]),
matches: vec![],
focus_handle: focus_handle.clone(),
@@ -226,6 +250,13 @@ impl KeymapEditor {
)
.await;
this.update(cx, |this, cx| {
match this.filter_state {
FilterState::Conflicts => {
matches.retain(|candidate| this.conflicts.contains(&candidate.candidate_id));
}
FilterState::All => {}
}
if query.is_empty() {
// apply default sort
// sorts by source precedence, and alphabetically by action name within each source
@@ -340,6 +371,30 @@ impl KeymapEditor {
let query = this.update(cx, |this, cx| {
let (key_bindings, string_match_candidates) =
Self::process_bindings(json_language, rust_language, cx);
let mut conflicts: HashMap<_, Vec<usize>> = HashMap::default();
key_bindings
.iter()
.enumerate()
.filter(|(_, binding)| !binding.keystroke_text.is_empty())
.for_each(|(index, binding)| {
conflicts
.entry(binding.get_action_mapping())
.or_default()
.push(index);
});
this.conflicts = conflicts
.into_values()
.filter(|indices| indices.len() > 1)
.flatten()
.collect();
if this.conflicts.is_empty() {
this.filter_state = FilterState::All;
}
this.keybindings = key_bindings;
this.string_match_candidates = Arc::new(string_match_candidates);
this.matches = this
@@ -466,11 +521,16 @@ impl KeymapEditor {
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
self.edit_selected_keybinding(window, cx);
self.open_edit_keybinding_modal(false, window, cx);
}
fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(keybind) = self.selected_binding() else {
fn open_edit_keybinding_modal(
&mut self,
create: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(keybind) = self.selected_binding().cloned() else {
return;
};
self.workspace
@@ -479,7 +539,7 @@ impl KeymapEditor {
let workspace_weak = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| {
let modal =
KeybindingEditorModal::new(keybind.clone(), workspace_weak, fs, window, cx);
KeybindingEditorModal::new(create, keybind, workspace_weak, fs, window, cx);
window.focus(&modal.focus_handle(cx));
modal
});
@@ -488,7 +548,11 @@ impl KeymapEditor {
}
fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
self.edit_selected_keybinding(window, cx);
self.open_edit_keybinding_modal(false, window, cx);
}
fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
self.open_edit_keybinding_modal(true, window, cx);
}
fn copy_context_to_clipboard(
@@ -536,7 +600,19 @@ struct ProcessedKeybinding {
source: Option<(KeybindSource, SharedString)>,
}
#[derive(Clone, Debug, IntoElement)]
impl ProcessedKeybinding {
fn get_action_mapping(&self) -> (SharedString, Option<SharedString>) {
(
self.keystroke_text.clone(),
self.context
.as_ref()
.and_then(|context| context.local())
.cloned(),
)
}
}
#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
enum KeybindContextString {
Global,
Local(SharedString, Arc<Language>),
@@ -561,14 +637,11 @@ impl KeybindContextString {
}
impl RenderOnce for KeybindContextString {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
match self {
KeybindContextString::Global => StyledText::new(KeybindContextString::GLOBAL.clone())
.with_highlights([(
0..KeybindContextString::GLOBAL.len(),
gpui::HighlightStyle::color(_cx.theme().colors().text_muted),
)])
.into_any_element(),
KeybindContextString::Global => {
muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
}
KeybindContextString::Local(name, language) => {
SyntaxHighlightedText::new(name, language).into_any_element()
}
@@ -576,6 +649,14 @@ impl RenderOnce for KeybindContextString {
}
}
fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
let len = text.len();
StyledText::new(text).with_highlights([(
0..len,
gpui::HighlightStyle::color(cx.theme().colors().text_muted),
)])
}
impl Item for KeymapEditor {
type Event = ();
@@ -600,6 +681,7 @@ impl Render for KeymapEditor {
.on_action(cx.listener(Self::focus_search))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::edit_binding))
.on_action(cx.listener(Self::create_binding))
.on_action(cx.listener(Self::copy_action_to_clipboard))
.on_action(cx.listener(Self::copy_context_to_clipboard))
.size_full()
@@ -620,7 +702,22 @@ impl Render for KeymapEditor {
.border_1()
.border_color(theme.colors().border)
.rounded_lg()
.child(self.filter_editor.clone()),
.child(self.filter_editor.clone())
.when(!self.conflicts.is_empty(), |this| {
this.child(
IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
.tooltip(Tooltip::text(match self.filter_state {
FilterState::All => "Show conflicts",
FilterState::Conflicts => "Hide conflicts",
}))
.selected_icon_color(Color::Error)
.toggle_state(matches!(self.filter_state, FilterState::Conflicts))
.on_click(cx.listener(|this, _, _, cx| {
this.filter_state = this.filter_state.invert();
this.update_matches(cx);
})),
)
}),
)
.child(
Table::new()
@@ -631,7 +728,7 @@ impl Render for KeymapEditor {
.uniform_list(
"keymap-editor-table",
row_count,
cx.processor(move |this, range: Range<usize>, _window, _cx| {
cx.processor(move |this, range: Range<usize>, _window, cx| {
range
.filter_map(|index| {
let candidate_id = this.matches.get(index)?.candidate_id;
@@ -661,12 +758,17 @@ impl Render for KeymapEditor {
binding.keystroke_text.clone().into_any_element(),
IntoElement::into_any_element,
);
let action_input = binding
.action_input
.clone()
.map_or(gpui::Empty.into_any_element(), |input| {
input.into_any_element()
});
let action_input = match binding.action_input.clone() {
Some(input) => input.into_any_element(),
None => {
if binding.action_schema.is_some() {
muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
.into_any_element()
} else {
gpui::Empty.into_any_element()
}
}
};
let context = binding
.context
.clone()
@@ -686,14 +788,22 @@ impl Render for KeymapEditor {
)
.map_row(
cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
let is_conflict = this
.matches
.get(row_index)
.map(|candidate| candidate.candidate_id)
.is_some_and(|id| this.conflicts.contains(&id));
let is_selected = this.selected_index == Some(row_index);
let row = row
.id(("keymap-table-row", row_index))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.selected_index = Some(row_index);
}))
.border_2()
.border_color(transparent_black())
.when(is_conflict, |row| {
row.bg(cx.theme().status().error_background)
})
.when(is_selected, |row| {
row.border_color(cx.theme().colors().panel_focused_border)
});
@@ -778,6 +888,7 @@ impl RenderOnce for SyntaxHighlightedText {
}
struct KeybindingEditorModal {
creating: bool,
editing_keybind: ProcessedKeybinding,
keybind_editor: Entity<KeystrokeInput>,
context_editor: Entity<Editor>,
@@ -798,13 +909,14 @@ impl Focusable for KeybindingEditorModal {
impl KeybindingEditorModal {
pub fn new(
create: bool,
editing_keybind: ProcessedKeybinding,
workspace: WeakEntity<Workspace>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut App,
) -> Self {
let keybind_editor = cx.new(KeystrokeInput::new);
let keybind_editor = cx.new(|cx| KeystrokeInput::new(window, cx));
let context_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
@@ -864,6 +976,7 @@ impl KeybindingEditorModal {
});
Self {
creating: create,
editing_keybind,
fs,
keybind_editor,
@@ -902,8 +1015,11 @@ impl KeybindingEditorModal {
return;
}
let create = self.creating;
cx.spawn(async move |this, cx| {
if let Err(err) = save_keybinding_update(
create,
existing_keybind,
&new_keystrokes,
new_context.as_deref(),
@@ -931,6 +1047,17 @@ impl KeybindingEditorModal {
impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors();
let input_base = || {
div()
.w_full()
.py_2()
.px_3()
.min_h_8()
.rounded_md()
.bg(theme.editor_background)
.border_1()
.border_color(theme.border_variant)
};
return v_flex()
.w(rems(34.))
@@ -938,12 +1065,11 @@ impl Render for KeybindingEditorModal {
.child(
v_flex()
.p_3()
.gap_2()
.child(Label::new("Edit Keystroke"))
.child(
v_flex().child(Label::new("Edit Keystroke")).child(
Label::new("Input the desired keystroke for the selected action.")
.color(Color::Muted),
),
Label::new("Input the desired keystroke for the selected action.")
.color(Color::Muted)
.mb_2(),
)
.child(self.keybind_editor.clone()),
)
@@ -951,49 +1077,27 @@ impl Render for KeybindingEditorModal {
this.child(
v_flex()
.p_3()
.gap_3()
.pt_0()
.child(Label::new("Edit Input"))
.child(
v_flex().child(Label::new("Edit Input")).child(
Label::new("Input the desired input to the binding.")
.color(Color::Muted),
),
Label::new("Input the desired input to the binding.")
.color(Color::Muted)
.mb_2(),
)
.child(
div()
.w_full()
.border_color(cx.theme().colors().border_variant)
.border_1()
.py_2()
.px_3()
.min_h_8()
.rounded_md()
.bg(theme.editor_background)
.child(editor),
),
.child(input_base().child(editor)),
)
})
.child(
v_flex()
.p_3()
.gap_3()
.pt_0()
.child(Label::new("Edit Context"))
.child(
v_flex().child(Label::new("Edit Context")).child(
Label::new("Input the desired context for the binding.")
.color(Color::Muted),
),
Label::new("Input the desired context for the binding.")
.color(Color::Muted)
.mb_2(),
)
.child(
div()
.w_full()
.border_color(cx.theme().colors().border_variant)
.border_1()
.py_2()
.px_3()
.min_h_8()
.rounded_md()
.bg(theme.editor_background)
.child(self.context_editor.clone()),
),
.child(input_base().child(self.context_editor.clone())),
)
.child(
h_flex()
@@ -1002,7 +1106,7 @@ impl Render for KeybindingEditorModal {
.gap_1()
.justify_end()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.border_color(theme.border_variant)
.child(
Button::new("cancel", "Cancel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
@@ -1142,6 +1246,7 @@ async fn load_rust_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp)
}
async fn save_keybinding_update(
create: bool,
existing: ProcessedKeybinding,
new_keystrokes: &[Keystroke],
new_context: Option<&str>,
@@ -1168,7 +1273,7 @@ async fn save_keybinding_update(
.as_ref()
.map(|input| input.text.as_ref());
let operation = if existing.ui_key_binding.is_some() {
let operation = if !create {
settings::KeybindUpdateOperation::Replace {
target: settings::KeybindUpdateTarget {
context: existing_context,
@@ -1190,7 +1295,13 @@ async fn save_keybinding_update(
},
}
} else {
anyhow::bail!("Adding new bindings not implemented yet");
settings::KeybindUpdateOperation::Add(settings::KeybindUpdateTarget {
context: new_context,
keystrokes: new_keystrokes,
action_name: &existing.action_name,
use_key_equivalents: false,
input,
})
};
let updated_keymap_contents =
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
@@ -1204,14 +1315,22 @@ async fn save_keybinding_update(
struct KeystrokeInput {
keystrokes: Vec<Keystroke>,
focus_handle: FocusHandle,
intercept_subscription: Option<Subscription>,
_focus_subscriptions: [Subscription; 2],
}
impl KeystrokeInput {
fn new(cx: &mut Context<Self>) -> Self {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let _focus_subscriptions = [
cx.on_focus_in(&focus_handle, window, Self::on_focus_in),
cx.on_focus_out(&focus_handle, window, Self::on_focus_out),
];
Self {
keystrokes: Vec::new(),
focus_handle,
intercept_subscription: None,
_focus_subscriptions,
}
}
@@ -1240,21 +1359,13 @@ impl KeystrokeInput {
cx.notify();
}
fn on_key_down(
&mut self,
event: &gpui::KeyDownEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if event.is_held {
return;
}
fn handle_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) {
if let Some(last) = self.keystrokes.last_mut()
&& last.key.is_empty()
{
*last = event.keystroke.clone();
} else {
self.keystrokes.push(event.keystroke.clone());
*last = keystroke.clone();
} else if Some(keystroke) != self.keystrokes.last() {
self.keystrokes.push(keystroke.clone());
}
cx.stop_propagation();
cx.notify();
@@ -1280,6 +1391,24 @@ impl KeystrokeInput {
cx.notify();
}
fn on_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
if self.intercept_subscription.is_none() {
let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| {
this.handle_keystroke(&event.keystroke, cx);
});
self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
}
}
fn on_focus_out(
&mut self,
_event: gpui::FocusOutEvent,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
self.intercept_subscription.take();
}
fn keystrokes(&self) -> &[Keystroke] {
if self
.keystrokes
@@ -1299,14 +1428,14 @@ impl Focusable for KeystrokeInput {
}
impl Render for KeystrokeInput {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors();
let is_focused = self.focus_handle.is_focused(window);
return h_flex()
.id("keybinding_input")
.track_focus(&self.focus_handle)
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
.on_key_down(cx.listener(Self::on_key_down))
.on_key_up(cx.listener(Self::on_key_up))
.focus(|mut style| {
style.border_color = Some(colors.border_focused);
@@ -1317,12 +1446,13 @@ impl Render for KeystrokeInput {
.gap_2()
.min_h_8()
.w_full()
.flex_1()
.justify_between()
.rounded_md()
.overflow_hidden()
.bg(colors.editor_background)
.border_1()
.rounded_md()
.flex_1()
.overflow_hidden()
.border_color(colors.border_variant)
.child(
h_flex()
.w_full()
@@ -1347,6 +1477,7 @@ impl Render for KeystrokeInput {
.child(
IconButton::new("backspace-btn", IconName::Delete)
.tooltip(Tooltip::text("Delete Keystroke"))
.when(!is_focused, |this| this.icon_color(Color::Muted))
.on_click(cx.listener(|this, _event, _window, cx| {
this.keystrokes.pop();
cx.notify();
@@ -1355,6 +1486,7 @@ impl Render for KeystrokeInput {
.child(
IconButton::new("clear-btn", IconName::Eraser)
.tooltip(Tooltip::text("Clear Keystrokes"))
.when(!is_focused, |this| this.icon_color(Color::Muted))
.on_click(cx.listener(|this, _event, _window, cx| {
this.keystrokes.clear();
cx.notify();
@@ -1378,16 +1510,19 @@ fn build_keybind_context_menu(
return menu;
};
let selected_binding_has_context = selected_binding
let selected_binding_has_no_context = selected_binding
.context
.as_ref()
.and_then(KeybindContextString::local)
.is_some();
.is_none();
menu.action("Edit Binding", Box::new(EditBinding))
let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none();
menu.action_disabled_when(selected_binding_is_unbound, "Edit", Box::new(EditBinding))
.action("Create", Box::new(CreateBinding))
.action("Copy action", Box::new(CopyAction))
.action_disabled_when(
!selected_binding_has_context,
selected_binding_has_no_context,
"Copy Context",
Box::new(CopyContext),
)

View File

@@ -1,4 +1,5 @@
use gpui::{Entity, OwnedMenu, OwnedMenuItem};
use settings::Settings;
#[cfg(not(target_os = "macos"))]
use gpui::{Action, actions};
@@ -11,6 +12,8 @@ use serde::Deserialize;
use smallvec::SmallVec;
use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use crate::title_bar_settings::TitleBarSettings;
#[cfg(not(target_os = "macos"))]
actions!(
app_menu,
@@ -242,15 +245,21 @@ impl ApplicationMenu {
cx.defer_in(window, move |_, window, cx| next_handle.show(window, cx));
}
pub fn all_menus_shown(&self) -> bool {
self.entries.iter().any(|entry| entry.handle.is_deployed())
pub fn all_menus_shown(&self, cx: &mut Context<Self>) -> bool {
show_menus(cx)
|| self.entries.iter().any(|entry| entry.handle.is_deployed())
|| self.pending_menu_open.is_some()
}
}
pub(crate) fn show_menus(cx: &mut App) -> bool {
TitleBarSettings::get_global(cx).show_menus
&& (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some())
}
impl Render for ApplicationMenu {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let all_menus_shown = self.all_menus_shown();
let all_menus_shown = self.all_menus_shown(cx);
if let Some(pending_menu_open) = self.pending_menu_open.take() {
if let Some(entry) = self

View File

@@ -1,6 +1,6 @@
use gpui::{
AnyElement, Context, Decorations, InteractiveElement, IntoElement, MouseButton, ParentElement,
Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px,
AnyElement, Context, Decorations, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px,
};
use smallvec::SmallVec;
use std::mem;
@@ -37,6 +37,18 @@ impl PlatformTitleBar {
px(32.)
}
pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
if window.is_window_active() && !self.should_move {
cx.theme().colors().title_bar_background
} else {
cx.theme().colors().title_bar_inactive_background
}
} else {
cx.theme().colors().title_bar_background
}
}
pub fn set_children<T>(&mut self, children: T)
where
T: IntoIterator<Item = AnyElement>,
@@ -50,15 +62,7 @@ impl Render for PlatformTitleBar {
let supported_controls = window.window_controls();
let decorations = window.window_decorations();
let height = Self::height(window);
let titlebar_color = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
if window.is_window_active() && !self.should_move {
cx.theme().colors().title_bar_background
} else {
cx.theme().colors().title_bar_inactive_background
}
} else {
cx.theme().colors().title_bar_background
};
let titlebar_color = self.title_bar_color(window, cx);
let close_action = Box::new(workspace::CloseWindow);
let children = mem::take(&mut self.children);

View File

@@ -8,7 +8,10 @@ mod title_bar_settings;
#[cfg(feature = "stories")]
mod stories;
use crate::{application_menu::ApplicationMenu, platform_title_bar::PlatformTitleBar};
use crate::{
application_menu::{ApplicationMenu, show_menus},
platform_title_bar::PlatformTitleBar,
};
#[cfg(not(target_os = "macos"))]
use crate::application_menu::{
@@ -133,6 +136,8 @@ impl Render for TitleBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let title_bar_settings = *TitleBarSettings::get_global(cx);
let show_menus = show_menus(cx);
let mut children = Vec::new();
children.push(
@@ -142,10 +147,14 @@ impl Render for TitleBar {
let mut render_project_items = title_bar_settings.show_branch_name
|| title_bar_settings.show_project_items;
title_bar
.when_some(self.application_menu.clone(), |title_bar, menu| {
render_project_items &= !menu.read(cx).all_menus_shown();
title_bar.child(menu)
})
.when_some(
self.application_menu.clone().filter(|_| !show_menus),
|title_bar, menu| {
render_project_items &=
!menu.update(cx, |menu, cx| menu.all_menus_shown(cx));
title_bar.child(menu)
},
)
.when(render_project_items, |title_bar| {
title_bar
.when(title_bar_settings.show_project_items, |title_bar| {
@@ -190,11 +199,39 @@ impl Render for TitleBar {
.into_any_element(),
);
self.platform_titlebar.update(cx, |this, _| {
this.set_children(children);
});
if show_menus {
self.platform_titlebar.update(cx, |this, _| {
this.set_children(
self.application_menu
.clone()
.map(|menu| menu.into_any_element()),
);
});
self.platform_titlebar.clone().into_any_element()
let height = PlatformTitleBar::height(window);
let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
platform_titlebar.title_bar_color(window, cx)
});
v_flex()
.w_full()
.child(self.platform_titlebar.clone().into_any_element())
.child(
h_flex()
.bg(title_bar_color)
.h(height)
.pl_2()
.justify_between()
.w_full()
.children(children),
)
.into_any_element()
} else {
self.platform_titlebar.update(cx, |this, _| {
this.set_children(children);
});
self.platform_titlebar.clone().into_any_element()
}
}
}

View File

@@ -11,6 +11,7 @@ pub struct TitleBarSettings {
pub show_branch_name: bool,
pub show_project_items: bool,
pub show_sign_in: bool,
pub show_menus: bool,
}
#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -39,6 +40,10 @@ pub struct TitleBarSettingsContent {
///
/// Default: true
pub show_sign_in: Option<bool>,
/// Whether to show the menus in the title bar.
///
/// Default: false
pub show_menus: Option<bool>,
}
impl Settings for TitleBarSettings {

Some files were not shown because too many files have changed in this diff Show More