Compare commits
77 Commits
v0.195.5
...
linux/keys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8276554222 | ||
|
|
2400ed34e0 | ||
|
|
ce23637dc1 | ||
|
|
7bcd6d839f | ||
|
|
20b9989b79 | ||
|
|
3269029a3e | ||
|
|
ea0e908714 | ||
|
|
8b69ccb488 | ||
|
|
2d6d53219b | ||
|
|
de615870cc | ||
|
|
f1f9470a14 | ||
|
|
9433c381eb | ||
|
|
6ede6a1573 | ||
|
|
7e0d74db8a | ||
|
|
e019ce8260 | ||
|
|
7ab7eab54e | ||
|
|
7da2e2af50 | ||
|
|
92a986cffd | ||
|
|
8b6658c650 | ||
|
|
9bc4c0f7f5 | ||
|
|
f829a178f6 | ||
|
|
2df80de880 | ||
|
|
2976b99b14 | ||
|
|
ad3d44119a | ||
|
|
0b5a264f88 | ||
|
|
f22d06cf6e | ||
|
|
26718da9bb | ||
|
|
671a91e69d | ||
|
|
bbda5d4f78 | ||
|
|
2fb674088f | ||
|
|
5c4ea49793 | ||
|
|
21e7cc3fed | ||
|
|
f53168c56c | ||
|
|
3182f14972 | ||
|
|
eca211e0f1 | ||
|
|
d6add799dc | ||
|
|
e9649dc25c | ||
|
|
71daf47ad5 | ||
|
|
66c6f5066e | ||
|
|
be79ccde07 | ||
|
|
061ba9b6d2 | ||
|
|
1ba3c2f589 | ||
|
|
bbee877ca8 | ||
|
|
c6e697fc7f | ||
|
|
b7b83105fc | ||
|
|
7d901c5e47 | ||
|
|
1a8d1944b0 | ||
|
|
35db644beb | ||
|
|
76e52ea374 | ||
|
|
ca0f0cc8d1 | ||
|
|
a133c1311d | ||
|
|
08ffd9884a | ||
|
|
dc591fe7c7 | ||
|
|
95784d53ca | ||
|
|
9b63ba6205 | ||
|
|
862e733ef5 | ||
|
|
66dda8e368 | ||
|
|
16d02cfdb3 | ||
|
|
2c41e10c98 | ||
|
|
9ab5e78b79 | ||
|
|
e30e4381de | ||
|
|
de627ba04d | ||
|
|
80eed63255 | ||
|
|
974bc4096a | ||
|
|
642d8bb8f5 | ||
|
|
41fe2a2ab4 | ||
|
|
96ff6d86a3 | ||
|
|
6e5763215f | ||
|
|
de0e6f716c | ||
|
|
93bfae71dc | ||
|
|
171be7e009 | ||
|
|
d5cc1cbaa9 | ||
|
|
6d26f107dd | ||
|
|
e2b9dfa89c | ||
|
|
495ec7a109 | ||
|
|
b9b42bee99 | ||
|
|
a9b82e1e57 |
13
.github/actions/run_tests/action.yml
vendored
@@ -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
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
1
assets/icons/ai_gemini.svg
Normal 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 |
@@ -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 |
@@ -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 |
@@ -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 |
3
assets/icons/tool_bulb.svg
Normal 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 |
4
assets/icons/tool_copy.svg
Normal 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 |
5
assets/icons/tool_delete_file.svg
Normal 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 |
5
assets/icons/tool_diagnostics.svg
Normal 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 |
3
assets/icons/tool_folder.svg
Normal 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 |
5
assets/icons/tool_hammer.svg
Normal 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 |
4
assets/icons/tool_notification.svg
Normal 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 |
4
assets/icons/tool_pencil.svg
Normal 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 |
7
assets/icons/tool_read.svg
Normal 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 |
4
assets/icons/tool_regex.svg
Normal 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 |
4
assets/icons/tool_search.svg
Normal 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 |
5
assets/icons/tool_terminal.svg
Normal 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 |
5
assets/icons/tool_web.svg
Normal 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 |
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
1645
crates/acp/src/acp.rs
Normal 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 {
|
||||
|
||||
27
crates/agent_servers/Cargo.toml
Normal 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
|
||||
1
crates/agent_servers/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
231
crates/agent_servers/src/agent_servers.rs
Normal 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) {}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
5
crates/agent_ui/src/acp.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod completion_provider;
|
||||
mod message_history;
|
||||
mod thread_view;
|
||||
|
||||
pub use thread_view::AcpThreadView;
|
||||
574
crates/agent_ui/src/acp/completion_provider.rs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
81
crates/agent_ui/src/acp/message_history.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
1972
crates/agent_ui/src/acp/thread_view.rs
Normal 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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -475,6 +475,7 @@ impl CodegenAlternative {
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
messages: vec![request_message],
|
||||
thinking_allowed: false,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -297,6 +297,7 @@ impl TerminalInlineAssistant {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
thinking_allowed: false,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, |_| {});
|
||||
|
||||
@@ -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 @@
|
||||
← 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) =>
|
||||
` ${p[0]}=${p[1]}`,
|
||||
)
|
||||
.map((p) => ` ${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) => ` ${p[0]}=${p[1]}`)
|
||||
.join(",<br/>");
|
||||
const formattedParams = params.map((p) => ` ${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();
|
||||
}
|
||||
|
||||
@@ -594,6 +594,7 @@ impl ExampleInstance {
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
let model = model.clone();
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1830,6 +1830,7 @@ impl GitPanel {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
thinking_allowed: false,
|
||||
};
|
||||
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -981,6 +981,7 @@ impl RulesLibrary {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
thinking_allowed: true,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -570,6 +570,7 @@ impl SummaryIndex {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
let code_len = code.len();
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||