Compare commits
211 Commits
window-dec
...
selectable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3903957c8 | ||
|
|
e106a39620 | ||
|
|
d32e9f759c | ||
|
|
15662f105e | ||
|
|
1887a6db53 | ||
|
|
fa9360f78d | ||
|
|
3ff738fa03 | ||
|
|
77b31d1845 | ||
|
|
15b8790a2c | ||
|
|
b693cbfcb7 | ||
|
|
73d7f70ff6 | ||
|
|
be5b7b2e70 | ||
|
|
4434353f73 | ||
|
|
95637a0320 | ||
|
|
ee623f77c1 | ||
|
|
c732865fc5 | ||
|
|
8a659af82c | ||
|
|
07dc4050bf | ||
|
|
896b9bda23 | ||
|
|
9282bf97ae | ||
|
|
33a67ad6b9 | ||
|
|
d4ddc4c62c | ||
|
|
8944af7406 | ||
|
|
1662993811 | ||
|
|
7ef64fe6db | ||
|
|
f147722fe0 | ||
|
|
e1a6efa609 | ||
|
|
ba7d5a3d4c | ||
|
|
6f99399224 | ||
|
|
2f2047ab22 | ||
|
|
d01d76482d | ||
|
|
a46a562dc2 | ||
|
|
4bb8a0845f | ||
|
|
c4bca874b6 | ||
|
|
46c0aa5fc2 | ||
|
|
2db06c1567 | ||
|
|
c59d5fbae7 | ||
|
|
8df098ff35 | ||
|
|
639b21a7c5 | ||
|
|
c65673feae | ||
|
|
6c9da838b7 | ||
|
|
a173beeb81 | ||
|
|
002ce6c343 | ||
|
|
2c30b8836e | ||
|
|
0d527dfb9e | ||
|
|
110ce8a267 | ||
|
|
c6b9f1920f | ||
|
|
df935df5a3 | ||
|
|
68b5ea4e60 | ||
|
|
5e1521eded | ||
|
|
ba28827de5 | ||
|
|
c22dbbebe2 | ||
|
|
8f29ff8a63 | ||
|
|
ae414e21f0 | ||
|
|
2dd486733b | ||
|
|
f44e81b3b5 | ||
|
|
c093bc8aa8 | ||
|
|
09e7b481b8 | ||
|
|
8cfa690271 | ||
|
|
3cdd465226 | ||
|
|
8203b6875b | ||
|
|
ce7074c883 | ||
|
|
6cc8412a05 | ||
|
|
2a97aad273 | ||
|
|
275dd3fa81 | ||
|
|
3cb2a1404c | ||
|
|
dd9b2e2cde | ||
|
|
b691d1baf2 | ||
|
|
bc0359a474 | ||
|
|
23c84f8dc0 | ||
|
|
29226170f1 | ||
|
|
9a523ef730 | ||
|
|
9b688655a8 | ||
|
|
9a6f30fd95 | ||
|
|
b3dad0bfcb | ||
|
|
18d6be250f | ||
|
|
5e1c690888 | ||
|
|
034d905435 | ||
|
|
0d7bd0c535 | ||
|
|
ed50dea042 | ||
|
|
5c95d2806b | ||
|
|
05e2e4d929 | ||
|
|
30479bf062 | ||
|
|
a40a16ab98 | ||
|
|
2e7db8f855 | ||
|
|
b0ecda6370 | ||
|
|
efc2336be5 | ||
|
|
9e36a66fec | ||
|
|
97f315356d | ||
|
|
0b6ef995d4 | ||
|
|
414cff5c14 | ||
|
|
2925f3d33c | ||
|
|
1a0242eff7 | ||
|
|
ea9ba6863d | ||
|
|
af697d9cc2 | ||
|
|
75377bbe0f | ||
|
|
032b203519 | ||
|
|
f555f66a8c | ||
|
|
d3f869acd8 | ||
|
|
c617d48e16 | ||
|
|
7f50055d70 | ||
|
|
c5e5add094 | ||
|
|
65e463b599 | ||
|
|
11c7374f76 | ||
|
|
cd7268f21f | ||
|
|
a1eaf1bb3c | ||
|
|
ab83820b6e | ||
|
|
800bdf34d5 | ||
|
|
79f3646325 | ||
|
|
813cc3f5e5 | ||
|
|
c190ed49da | ||
|
|
5c7e6b7eff | ||
|
|
1c1fd6aaa1 | ||
|
|
750df6c93d | ||
|
|
a53b3b6b10 | ||
|
|
078ce330c6 | ||
|
|
19490d8806 | ||
|
|
61e4b6413a | ||
|
|
950e7e5414 | ||
|
|
68accaeb00 | ||
|
|
ca27f42a9d | ||
|
|
d70c577293 | ||
|
|
c77ea47f43 | ||
|
|
821aa0811d | ||
|
|
fa602001e3 | ||
|
|
9b7bc04a87 | ||
|
|
a61188d137 | ||
|
|
1bd585186a | ||
|
|
e69f9d6cf9 | ||
|
|
c4dbe32f20 | ||
|
|
f2711b2fca | ||
|
|
1260b52c82 | ||
|
|
fc8749ffd7 | ||
|
|
2a923e338f | ||
|
|
56e3fc794a | ||
|
|
97cfe3a4b7 | ||
|
|
36f044f48e | ||
|
|
874c1f773b | ||
|
|
67229ea673 | ||
|
|
0423ad2c43 | ||
|
|
948f16e2e3 | ||
|
|
66dce927dc | ||
|
|
b0edf0ec7b | ||
|
|
c8b106245c | ||
|
|
398c2f91dd | ||
|
|
3a5d116ffe | ||
|
|
e3cd1dd2d0 | ||
|
|
b1f8fc88a1 | ||
|
|
d450a1d9e6 | ||
|
|
840c6e414a | ||
|
|
3ef6efbb9d | ||
|
|
a15673f778 | ||
|
|
b6e1acbf71 | ||
|
|
b41f02d11a | ||
|
|
f78a566073 | ||
|
|
1574eddb46 | ||
|
|
2d3e2a4af0 | ||
|
|
f4615f3b3a | ||
|
|
c6269f549d | ||
|
|
c5d20e1215 | ||
|
|
15c3cc14e0 | ||
|
|
d640da2b77 | ||
|
|
6becb8c859 | ||
|
|
6aa53496c3 | ||
|
|
ba19bccd66 | ||
|
|
6fad3883cf | ||
|
|
c1881b44ab | ||
|
|
b3646b32b2 | ||
|
|
89c04a79ae | ||
|
|
667d9825b6 | ||
|
|
f83f69a450 | ||
|
|
8e1103a916 | ||
|
|
c7db6c4bf3 | ||
|
|
e4f0cf526e | ||
|
|
8ec6b00106 | ||
|
|
37d62cf590 | ||
|
|
b87b4fcfdf | ||
|
|
9097c9151b | ||
|
|
54e48cf7da | ||
|
|
89726f6216 | ||
|
|
2fd07bfa46 | ||
|
|
23d924c33f | ||
|
|
e7a9ba9cb3 | ||
|
|
4397b8c6fc | ||
|
|
965f2dbadb | ||
|
|
3ee62fdf0c | ||
|
|
8b921f87d6 | ||
|
|
00f78b94fe | ||
|
|
58165d8492 | ||
|
|
e5dd34ed5d | ||
|
|
4605db7523 | ||
|
|
6f3250ba9e | ||
|
|
998bd35d6f | ||
|
|
87eaa625de | ||
|
|
4b99a94470 | ||
|
|
a4be555dc9 | ||
|
|
a06eb30f8d | ||
|
|
14a0a130f3 | ||
|
|
e0b46a5060 | ||
|
|
8cea761693 | ||
|
|
e2087a0a84 | ||
|
|
a901f9976b | ||
|
|
bfc7932024 | ||
|
|
1e825ffea5 | ||
|
|
511463f574 | ||
|
|
7dc6ba4d66 | ||
|
|
8781db6b68 | ||
|
|
77809989aa | ||
|
|
3125c98bb9 | ||
|
|
8d1d06f0b3 | ||
|
|
f52d4520e9 |
@@ -1,6 +1,13 @@
|
||||
.git
|
||||
.github
|
||||
**/.gitignore
|
||||
**/.gitkeep
|
||||
.gitattributes
|
||||
.mailmap
|
||||
**/target
|
||||
zed.xcworkspace
|
||||
.DS_Store
|
||||
compose.yml
|
||||
plugins/bin
|
||||
script/node_modules
|
||||
styles/node_modules
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
5
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -32,9 +32,10 @@ body:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
label: If applicable, attach your Zed.log file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
macOS: `~/Library/Logs/Zed/Zed.log`
|
||||
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary><pre>
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
Release Notes:
|
||||
|
||||
- Added/Fixed/Improved ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
|
||||
- Added/Fixed/Improved ... ([#NNNNN](https://github.com/zed-industries/zed/issues/NNNNN)).
|
||||
|
||||
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
|
||||
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -319,7 +319,6 @@ jobs:
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/.direnv
|
||||
.idea
|
||||
**/target
|
||||
**/cargo-target
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
},
|
||||
"JSON": {
|
||||
"tab_size": 2,
|
||||
"preferred_line_length": 100,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"JSONC": {
|
||||
"tab_size": 2,
|
||||
"preferred_line_length": 100,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"JavaScript": {
|
||||
|
||||
97
Cargo.lock
generated
97
Cargo.lock
generated
@@ -341,9 +341,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
|
||||
dependencies = [
|
||||
"async-fs 2.1.1",
|
||||
"async-net 2.0.0",
|
||||
@@ -378,6 +377,7 @@ dependencies = [
|
||||
"cargo_toml",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"ctor",
|
||||
@@ -420,6 +420,7 @@ dependencies = [
|
||||
"telemetry_events",
|
||||
"terminal",
|
||||
"terminal_view",
|
||||
"text",
|
||||
"theme",
|
||||
"tiktoken-rs",
|
||||
"toml 0.8.10",
|
||||
@@ -2406,6 +2407,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -2464,6 +2466,7 @@ version = "0.44.0"
|
||||
dependencies = [
|
||||
"anthropic",
|
||||
"anyhow",
|
||||
"assistant",
|
||||
"async-trait",
|
||||
"async-tungstenite",
|
||||
"audio",
|
||||
@@ -3602,6 +3605,7 @@ dependencies = [
|
||||
"linkify",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"multi_buffer",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot",
|
||||
@@ -3933,6 +3937,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"snippet_provider",
|
||||
"task",
|
||||
"theme",
|
||||
"toml 0.8.10",
|
||||
@@ -4889,11 +4894,9 @@ dependencies = [
|
||||
"log",
|
||||
"media",
|
||||
"metal",
|
||||
"mio 1.0.0",
|
||||
"num_cpus",
|
||||
"objc",
|
||||
"oo7",
|
||||
"open",
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
@@ -5691,7 +5694,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"tempfile",
|
||||
@@ -5705,25 +5708,6 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isahc"
|
||||
version = "1.7.2"
|
||||
@@ -6640,19 +6624,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.6.0"
|
||||
@@ -6891,7 +6862,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
@@ -7225,17 +7196,6 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "open_ai"
|
||||
version = "0.1.0"
|
||||
@@ -7401,6 +7361,7 @@ dependencies = [
|
||||
"db",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
"language",
|
||||
@@ -8086,6 +8047,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smol",
|
||||
"snippet",
|
||||
"snippet_provider",
|
||||
"task",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
@@ -8313,6 +8275,7 @@ dependencies = [
|
||||
"assistant",
|
||||
"editor",
|
||||
"gpui",
|
||||
"repl",
|
||||
"search",
|
||||
"settings",
|
||||
"ui",
|
||||
@@ -8539,7 +8502,7 @@ dependencies = [
|
||||
"task",
|
||||
"terminal_view",
|
||||
"ui",
|
||||
"ui_text_field",
|
||||
"ui_input",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
@@ -9902,6 +9865,22 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snippet_provider"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snippet",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.9"
|
||||
@@ -10282,6 +10261,7 @@ dependencies = [
|
||||
"story",
|
||||
"strum",
|
||||
"theme",
|
||||
"title_bar",
|
||||
"ui",
|
||||
]
|
||||
|
||||
@@ -11112,7 +11092,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"bytes 1.5.0",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
@@ -11737,7 +11717,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ui_text_field"
|
||||
name = "ui_input"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
@@ -13607,7 +13587,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.144.0"
|
||||
version = "0.145.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -13682,6 +13662,7 @@ dependencies = [
|
||||
"settings",
|
||||
"simplelog",
|
||||
"smol",
|
||||
"snippet_provider",
|
||||
"supermaven",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
@@ -13720,9 +13701,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_clojure"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13834,14 +13815,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_lua"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_ocaml"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -88,6 +88,7 @@ members = [
|
||||
"crates/semantic_version",
|
||||
"crates/settings",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/story",
|
||||
@@ -108,7 +109,7 @@ members = [
|
||||
"crates/time_format",
|
||||
"crates/title_bar",
|
||||
"crates/ui",
|
||||
"crates/ui_text_field",
|
||||
"crates/ui_input",
|
||||
"crates/util",
|
||||
"crates/vcs_menu",
|
||||
"crates/vim",
|
||||
@@ -239,6 +240,7 @@ semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
settings = { path = "crates/settings" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
snippet_provider = { path = "crates/snippet_provider" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
story = { path = "crates/story" }
|
||||
@@ -259,7 +261,7 @@ theme_selector = { path = "crates/theme_selector" }
|
||||
time_format = { path = "crates/time_format" }
|
||||
title_bar = { path = "crates/title_bar" }
|
||||
ui = { path = "crates/ui" }
|
||||
ui_text_field = { path = "crates/ui_text_field" }
|
||||
ui_input = { path = "crates/ui_input" }
|
||||
util = { path = "crates/util" }
|
||||
vcs_menu = { path = "crates/vcs_menu" }
|
||||
vim = { path = "crates/vim" }
|
||||
@@ -272,9 +274,9 @@ zed_actions = { path = "crates/zed_actions" }
|
||||
alacritty_terminal = "0.23"
|
||||
any_vec = "0.13"
|
||||
anyhow = "1.0.57"
|
||||
ashpd = "0.8.0"
|
||||
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = { version = "0.1"}
|
||||
async-dispatcher = { version = "0.1" }
|
||||
async-fs = "1.6"
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
@@ -315,7 +317,9 @@ image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "1"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding" ] }
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
] }
|
||||
itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
@@ -341,7 +345,9 @@ rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] }
|
||||
runtimelib = { version = "0.12", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
schemars = "0.8"
|
||||
@@ -512,7 +518,7 @@ single_range_in_vec_init = "allow"
|
||||
|
||||
# There are a bunch of rules currently failing in the `style` group, so
|
||||
# allow all of those, for now.
|
||||
style = "allow"
|
||||
style = { level = "allow", priority = -1 }
|
||||
|
||||
# Individual rules that have violations in the codebase:
|
||||
almost_complete_range = "allow"
|
||||
|
||||
32
README.md
32
README.md
@@ -4,42 +4,36 @@
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
--------
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
### Installation
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
<a href="https://repology.org/project/zed-editor/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/zed-editor.svg?minversion=0.143.5" alt="Packaging status" align="right">
|
||||
</a>
|
||||
|
||||
On macOS and Linux you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
|
||||
|
||||
Other platforms are not yet available:
|
||||
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew install --cask zed@preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
### Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/development/macos.md)
|
||||
- [Building Zed for Linux](./docs/src/development/linux.md)
|
||||
- [Building Zed for Windows](./docs/src/development/windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
|
||||
|
||||
## Contributing
|
||||
### Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
### Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
|
||||
14
assets/icons/repl_neutral.svg
Normal file
14
assets/icons/repl_neutral.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_32_58)">
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_32_58">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
20
assets/icons/repl_off.svg
Normal file
20
assets/icons/repl_off.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_39_129)">
|
||||
<path d="M22.0209 11.9553C22.0059 10.0068 21.4219 8.10512 20.3408 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.1001 2.18C11.355 1.93537 12.1493 1.93674 13.5027 2.10594" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.8198 10.1C22.0644 11.3548 22.0644 12.6451 21.8198 13.9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20.2898 17.6C19.5716 18.6622 18.6548 19.5757 17.5898 20.29" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.9008 21.82C12.6459 22.0644 11.6432 22.1543 10.3883 21.91" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.18005 13.9C1.93543 12.6451 1.93543 11.3548 2.18005 10.1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.70996 6.40002C4.42822 5.33775 5.34503 4.42433 6.40996 3.71002" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.99072 12.0748C2.00804 14.0118 2.58758 15.9021 3.65891 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_39_129">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
15
assets/icons/repl_pause.svg
Normal file
15
assets/icons/repl_pause.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_32_70)">
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 15V9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 15V9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_32_70">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
14
assets/icons/repl_play.svg
Normal file
14
assets/icons/repl_play.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_32_64)">
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 8.56055C10 8.32095 10.267 8.17803 10.4664 8.31094L15.6256 11.7504C15.8037 11.8691 15.8037 12.1309 15.6256 12.2496L10.4664 15.6891C10.267 15.822 10 15.6791 10 15.4394V8.56055Z" fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_32_64">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -3,10 +3,14 @@
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
"home": "menu::SelectFirst",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"tab": "menu::SelectNext",
|
||||
"end": "menu::SelectLast",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
@@ -41,9 +45,10 @@
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
// "ctrl-t": "editor::Transpose",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-insert": "editor::Copy",
|
||||
"ctrl-c": "editor::Copy",
|
||||
@@ -83,54 +88,19 @@
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"ctrl-l": "editor::SelectLine",
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
// "cmd-shift-left": [
|
||||
// "editor::SelectToBeginningOfLine",
|
||||
// {
|
||||
// "stop_at_soft_wraps": true
|
||||
// }
|
||||
// ],
|
||||
"shift-home": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
// "ctrl-shift-a": [
|
||||
// "editor::SelectToBeginningOfLine",
|
||||
// {
|
||||
// "stop_at_soft_wraps": true
|
||||
// }
|
||||
// ],
|
||||
// "cmd-shift-right": [
|
||||
// "editor::SelectToEndOfLine",
|
||||
// {
|
||||
// "stop_at_soft_wraps": true
|
||||
// }
|
||||
// ],
|
||||
"shift-end": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
// "ctrl-shift-e": [
|
||||
// "editor::SelectToEndOfLine",
|
||||
// {
|
||||
// "stop_at_soft_wraps": true
|
||||
// }
|
||||
// ],
|
||||
// "alt-v": [
|
||||
// "editor::MovePageUp",
|
||||
// {
|
||||
// "center_cursor": true
|
||||
// }
|
||||
// ],
|
||||
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true }],
|
||||
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-alt-space": "editor::ShowCharacterPalette",
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
|
||||
"ctrl-'": "editor::ToggleHunkDiff",
|
||||
"ctrl-\"": "editor::ExpandAllHunkDiffs",
|
||||
"ctrl-alt-g b": "editor::ToggleGitBlame"
|
||||
"alt-g b": "editor::ToggleGitBlame"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -142,18 +112,8 @@
|
||||
"ctrl-enter": "editor::NewlineAbove",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
// "cmd-e": [
|
||||
// "buffer_search::Deploy",
|
||||
// {
|
||||
// "focus": false
|
||||
// }
|
||||
// ],
|
||||
"ctrl-h": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
|
||||
@@ -268,6 +228,7 @@
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-w": "pane::CloseActiveItem",
|
||||
"ctrl-f4": "pane::CloseActiveItem",
|
||||
"alt-ctrl-t": "pane::CloseInactiveItems",
|
||||
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-k u": "pane::CloseCleanItems",
|
||||
@@ -310,38 +271,13 @@
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding
|
||||
"ctrl-shift-down": "editor::SelectSmallerSyntaxNode", //todo(linux) tmp keybinding
|
||||
"ctrl-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"ctrl-shift-l": "editor::SelectAllMatches",
|
||||
"ctrl-shift-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"ctrl-k ctrl-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-k ctrl-shift-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": false
|
||||
}
|
||||
],
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
@@ -349,16 +285,23 @@
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"ctrl-shift-f10": "editor::GoToDefinitionSplit",
|
||||
"ctrl-f12": "editor::GoToTypeDefinition",
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-shift-\\": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-shift-[": "editor::Fold",
|
||||
"ctrl-shift-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"alt-ctrl-r": "editor::RevealInFinder",
|
||||
"alt-ctrl-r": "editor::RevealInFileManager",
|
||||
"ctrl-k r": "editor::RevealInFileManager",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
"ctrl-\\": "pane::SplitRight",
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
@@ -383,8 +326,10 @@
|
||||
"alt-9": ["pane::ActivateItem", 8],
|
||||
"alt-0": "pane::ActivateLastItem",
|
||||
"ctrl-alt--": "pane::GoBack",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"ctrl-alt-shift--": "pane::GoForward",
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus"
|
||||
}
|
||||
},
|
||||
@@ -392,12 +337,7 @@
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "alt-cmd-o": [
|
||||
// "projects::OpenRecent",
|
||||
// {
|
||||
// "create_new_window": true
|
||||
// }
|
||||
// ]
|
||||
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
|
||||
"alt-ctrl-o": "projects::OpenRecent",
|
||||
"alt-ctrl-shift-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
@@ -416,25 +356,21 @@
|
||||
"alt-7": ["workspace::ActivatePane", 6],
|
||||
"alt-8": ["workspace::ActivatePane", 7],
|
||||
"alt-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-alt-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-alt-b": "workspace::ToggleRightDock",
|
||||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": [
|
||||
"pane::DeploySearch",
|
||||
{
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymap",
|
||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||
"ctrl-shift-t": "project_symbols::Toggle",
|
||||
"ctrl-t": "project_symbols::Toggle",
|
||||
"ctrl-p": "file_finder::Toggle",
|
||||
"ctrl-tab": "tab_switcher::Toggle",
|
||||
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
|
||||
"ctrl-e": "file_finder::Toggle",
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"f1": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
@@ -450,6 +386,7 @@
|
||||
"ctrl-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-shift-x": "zed::Extensions",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
@@ -466,7 +403,7 @@
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
// "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
@@ -565,11 +502,12 @@
|
||||
{
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"ctrl-alt-c": "outline_panel::CopyPath",
|
||||
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
|
||||
"alt-ctrl-r": "outline_panel::RevealInFinder",
|
||||
"alt-ctrl-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev"
|
||||
@@ -596,7 +534,7 @@
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFinder",
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
"home": "menu::SelectFirst",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"tab": "menu::SelectNext",
|
||||
"end": "menu::SelectLast",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
@@ -109,54 +113,14 @@
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-l": "editor::SelectLine",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
"cmd-shift-left": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"shift-home": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-a": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"cmd-shift-right": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"shift-end": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-e": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-v": [
|
||||
"editor::MovePageDown",
|
||||
{
|
||||
"center_cursor": true
|
||||
}
|
||||
],
|
||||
"alt-v": [
|
||||
"editor::MovePageUp",
|
||||
{
|
||||
"center_cursor": true
|
||||
}
|
||||
],
|
||||
"cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
|
||||
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
|
||||
"cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-v": ["editor::MovePageDown", { "center_cursor": true }],
|
||||
"alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-alt-z": "editor::RevertSelectedHunks",
|
||||
@@ -171,32 +135,22 @@
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"cmd-shift-enter": "editor::NewlineAbove",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"cmd-f": "buffer_search::Deploy",
|
||||
"cmd-alt-f": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-alt-l": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"selection_search_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-e": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"focus": false
|
||||
}
|
||||
],
|
||||
"cmd-alt-f": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
|
||||
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && !jupyter",
|
||||
"bindings": {
|
||||
"cmd-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && inline_completion",
|
||||
"bindings": {
|
||||
@@ -285,6 +239,7 @@
|
||||
"context": "ProjectSearchBar",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"cmd-shift-j": "project_search::ToggleFilters",
|
||||
"cmd-shift-f": "search::FocusSearch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
@@ -309,6 +264,7 @@
|
||||
"context": "ProjectSearchView",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"cmd-shift-j": "project_search::ToggleFilters",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
"alt-cmd-x": "search::ToggleRegex"
|
||||
@@ -356,38 +312,13 @@
|
||||
"alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode",
|
||||
"cmd-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"cmd-shift-l": "editor::SelectAllMatches",
|
||||
"ctrl-cmd-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"cmd-k cmd-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-k ctrl-cmd-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": false
|
||||
}
|
||||
],
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
@@ -400,11 +331,17 @@
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"cmd-shift-\\": "editor::MoveToEnclosingBracket",
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"cmd-.": "editor::ToggleCodeActions",
|
||||
"alt-cmd-r": "editor::RevealInFinder",
|
||||
"alt-cmd-r": "editor::RevealInFileManager",
|
||||
"cmd-k r": "editor::RevealInFileManager",
|
||||
"cmd-k p": "editor::CopyPath",
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
@@ -429,7 +366,7 @@
|
||||
"ctrl-9": ["pane::ActivateItem", 8],
|
||||
"ctrl-0": "pane::ActivateLastItem",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"ctrl-_": "pane::GoForward",
|
||||
"ctrl-shift--": "pane::GoForward",
|
||||
"cmd-shift-t": "pane::ReopenClosedItem",
|
||||
"cmd-shift-f": "project_search::ToggleFocus"
|
||||
}
|
||||
@@ -438,12 +375,7 @@
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "alt-cmd-o": [
|
||||
// "projects::OpenRecent",
|
||||
// {
|
||||
// "create_new_window": true
|
||||
// }
|
||||
// ]
|
||||
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
@@ -467,12 +399,7 @@
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"alt-cmd-y": "workspace::CloseAllDocks",
|
||||
"cmd-shift-f": "pane::DeploySearch",
|
||||
"cmd-shift-h": [
|
||||
"pane::DeploySearch",
|
||||
{
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"cmd-t": "project_symbols::Toggle",
|
||||
@@ -495,6 +422,7 @@
|
||||
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"cmd-shift-x": "zed::Extensions",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
@@ -595,11 +523,12 @@
|
||||
{
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"cmd-alt-c": "outline_panel::CopyPath",
|
||||
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
|
||||
"alt-cmd-r": "outline_panel::RevealInFinder",
|
||||
"alt-cmd-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev"
|
||||
@@ -619,12 +548,14 @@
|
||||
"cmd-alt-c": "project_panel::CopyPath",
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"enter": "project_panel::Rename",
|
||||
"f2": "project_panel::Rename",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
@@ -637,6 +568,12 @@
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"cmd-enter": "repl::Run"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
@@ -707,10 +644,14 @@
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
"cmd-up": "terminal::ScrollPageUp",
|
||||
"cmd-down": "terminal::ScrollPageDown",
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown",
|
||||
"shift-up": "terminal::ScrollLineUp",
|
||||
"shift-down": "terminal::ScrollLineDown",
|
||||
"cmd-home": "terminal::ScrollToTop",
|
||||
"cmd-end": "terminal::ScrollToBottom",
|
||||
"shift-home": "terminal::ScrollToTop",
|
||||
"shift-end": "terminal::ScrollToBottom"
|
||||
}
|
||||
|
||||
93
assets/keymaps/linux/atom.json
Normal file
93
assets/keymaps/linux/atom.json
Normal file
@@ -0,0 +1,93 @@
|
||||
// Default Keymap (Atom) for Zed on Linux
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-shift-f5": "workspace::Reload", // window:reload
|
||||
"ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane
|
||||
"ctrl-k ctrl-p": "workspace::ActivateNextPane" // window:focus-previous-pane
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
|
||||
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
|
||||
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
|
||||
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
|
||||
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
|
||||
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
||||
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
||||
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
|
||||
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
|
||||
"ctrl-j": "editor::JoinLines", // editor:join-lines
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
|
||||
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
|
||||
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
|
||||
"ctrl-shift-f3": "search::SelectPrevMatch" // find-and-replace:find-previous-selected
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder
|
||||
"ctrl-r": "project_symbols::Toggle" // symbols-view:toggle-project-symbols
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
// "ctrl-0": "project_panel::ToggleFocus", // tree-view:toggle-focus
|
||||
"ctrl-1": ["pane::ActivateItem", 0], // tree-view:open-selected-entry-in-pane-1
|
||||
"ctrl-2": ["pane::ActivateItem", 1], // tree-view:open-selected-entry-in-pane-2
|
||||
"ctrl-3": ["pane::ActivateItem", 2], // tree-view:open-selected-entry-in-pane-3
|
||||
"ctrl-4": ["pane::ActivateItem", 3], // tree-view:open-selected-entry-in-pane-4
|
||||
"ctrl-5": ["pane::ActivateItem", 4], // tree-view:open-selected-entry-in-pane-5
|
||||
"ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6
|
||||
"ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7
|
||||
"ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8
|
||||
"ctrl-9": ["pane::ActivateItem", 8] // tree-view:open-selected-entry-in-pane-9
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"f2": "project_panel::Rename", // tree-view:rename
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"ctrl-x": "project_panel::Cut", // tree-view:cut
|
||||
"ctrl-c": "project_panel::Copy", // tree-view:copy
|
||||
"ctrl-v": "project_panel::Paste" // tree-view:paste
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-shift-c": "project_panel::CopyPath", // tree-view:copy-full-path
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory
|
||||
"ctrl-]": "project_panel::ExpandSelectedEntry", // tree-view:expand-item
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry", // tree-view:expand-item
|
||||
"a": "project_panel::NewFile", // tree-view:add-file
|
||||
"d": "project_panel::Duplicate", // tree-view:duplicate
|
||||
"home": "menu::SelectFirst", // core:move-to-top
|
||||
"end": "menu::SelectLast", // core:move-to-bottom
|
||||
"shift-a": "project_panel::NewDirectory" // tree-view:add-folder
|
||||
}
|
||||
}
|
||||
]
|
||||
90
assets/keymaps/linux/jetbrains.json
Normal file
90
assets/keymaps/linux/jetbrains.json
Normal file
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-d": "editor::DuplicateLineDown",
|
||||
"ctrl-y": "editor::DeleteLine",
|
||||
"ctrl-pagedown": "editor::MovePageDown",
|
||||
"ctrl-pageup": "editor::MovePageUp",
|
||||
// "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"ctrl-alt-enter": "editor::NewlineAbove",
|
||||
"shift-enter": "editor::NewlineBelow",
|
||||
// "ctrl--": "editor::Fold", // TODO: `ctrl-numpad--` (numpad not implemented)
|
||||
// "ctrl-+": "editor::UnfoldLines", // TODO: `ctrl-numpad+` (numpad not implemented)
|
||||
"alt-shift-g": "editor::SplitSelectionIntoLines",
|
||||
"alt-j": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"alt-shift-j": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": true }],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-l": "editor::Format",
|
||||
"shift-f6": "editor::Rename",
|
||||
"ctrl-alt-left": "pane::GoBack",
|
||||
"ctrl-alt-right": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
"ctrl-alt-f7": "editor::FindAllReferences",
|
||||
// "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
||||
// "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock
|
||||
"ctrl-shift-b": "editor::GoToTypeDefinition",
|
||||
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"shift-f2": "editor::GoToPrevDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-f12": "outline::Toggle",
|
||||
"alt-7": "outline::Toggle",
|
||||
"ctrl-shift-n": "file_finder::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle",
|
||||
"alt-enter": "editor::ToggleCodeActions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-shift-n": "file_finder::Toggle",
|
||||
"ctrl-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
"ctrl-alt-shift-n": "project_symbols::Toggle",
|
||||
"alt-1": "workspace::ToggleLeftDock",
|
||||
"ctrl-e": "tab_switcher::Toggle",
|
||||
"alt-6": "diagnostics::Deploy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-alt-left": "pane::GoBack",
|
||||
"ctrl-alt-right": "pane::GoForward"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"enter": "project_panel::Open",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
}
|
||||
]
|
||||
53
assets/keymaps/linux/sublime_text.json
Normal file
53
assets/keymaps/linux/sublime_text.json
Normal file
@@ -0,0 +1,53 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"f4": "search::SelectNextMatch",
|
||||
"shift-f4": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
|
||||
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
|
||||
"shift-ctrl-r": "project_symbols::Toggle"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,8 @@
|
||||
// Default Keymap (Atom) for Zed on MacOS
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-cmd-l": "workspace::Reload",
|
||||
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
|
||||
"cmd-k cmd-n": "workspace::ActivateNextPane"
|
||||
}
|
||||
@@ -8,24 +10,22 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle",
|
||||
"cmd-|": "pane::RevealInProjectPanel",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"alt-cmd-b": "editor::GoToDefinitionSplit",
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-g": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-g": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"alt-enter": "editor::Newline",
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
@@ -74,21 +74,22 @@
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"cmd-x": "project_panel::Cut",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||
"alt-b": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-]": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-shift-c": "project_panel::CopyPath"
|
||||
"cmd-v": "project_panel::Paste"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-shift-c": "project_panel::CopyPath",
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-]": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"a": "project_panel::NewFile",
|
||||
"shift-a": "project_panel::NewDirectory",
|
||||
"shift-d": "project_panel::Duplicate"
|
||||
"d": "project_panel::Duplicate",
|
||||
"home": "menu::SelectFirst",
|
||||
"end": "menu::SelectLast",
|
||||
"shift-a": "project_panel::NewDirectory"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -21,24 +21,9 @@
|
||||
"cmd--": "editor::Fold",
|
||||
"cmd-+": "editor::UnfoldLines",
|
||||
"alt-shift-g": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-g": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-g": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": true
|
||||
}
|
||||
],
|
||||
"ctrl-g": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"ctrl-cmd-g": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": true }],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
@@ -54,7 +39,7 @@
|
||||
"cmd-shift-b": "editor::GoToTypeDefinition",
|
||||
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"cmd-f2": "editor::GoToPrevDiagnostic",
|
||||
"shift-f2": "editor::GoToPrevDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"cmd-home": "editor::MoveToBeginning",
|
||||
@@ -6,8 +6,7 @@
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize"
|
||||
"ctrl-tab": "pane::ActivatePrevItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -26,6 +25,9 @@
|
||||
"alt-shift-cmd-down": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
@@ -22,34 +22,14 @@
|
||||
"alt-shift-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"alt-left": [
|
||||
"editor::MoveToPreviousWordStart",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-right": [
|
||||
"editor::MoveToNextWordEnd",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }],
|
||||
"alt-right": ["editor::MoveToNextWordEnd", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
||||
"cmd-shift-left": "editor::SelectToBeginningOfLine",
|
||||
"cmd-shift-right": "editor::SelectToEndOfLine",
|
||||
"alt-shift-left": [
|
||||
"editor::SelectToPreviousWordStart",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-shift-right": [
|
||||
"editor::SelectToNextWordEnd",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-shift-left": ["editor::SelectToPreviousWordStart", { "stop_at_soft_wraps": true }],
|
||||
"alt-shift-right": ["editor::SelectToNextWordEnd", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
"ctrl-w": "editor::SelectNext",
|
||||
@@ -8,22 +8,8 @@
|
||||
{
|
||||
"context": "Editor && VimControl && !VimWaiting && !menu",
|
||||
"bindings": {
|
||||
"i": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"a": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
":": "command_palette::Toggle",
|
||||
"h": "vim::Left",
|
||||
"left": "vim::Left",
|
||||
@@ -31,6 +17,7 @@
|
||||
"j": "vim::Down",
|
||||
"down": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"ctrl-m": "vim::NextLineStart",
|
||||
"tab": "vim::Tab",
|
||||
"shift-tab": "vim::Tab",
|
||||
"k": "vim::Up",
|
||||
@@ -57,92 +44,25 @@
|
||||
// "b": "vim::PreviousSubwordStart",
|
||||
// "e": "vim::NextSubwordEnd",
|
||||
// "g e": "vim::PreviousSubwordEnd",
|
||||
"shift-w": [
|
||||
"vim::NextWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"shift-e": [
|
||||
"vim::NextWordEnd",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"shift-b": [
|
||||
"vim::PreviousWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"g shift-e": [
|
||||
"vim::PreviousWordEnd",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"shift-w": ["vim::NextWordStart", { "ignorePunctuation": true }],
|
||||
"shift-e": ["vim::NextWordEnd", { "ignorePunctuation": true }],
|
||||
"shift-b": ["vim::PreviousWordStart", { "ignorePunctuation": true }],
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
"/": "vim::Search",
|
||||
"g /": "pane::DeploySearch",
|
||||
"?": [
|
||||
"vim::Search",
|
||||
{
|
||||
"backwards": true
|
||||
}
|
||||
],
|
||||
"?": ["vim::Search", { "backwards": true }],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"n": "vim::MoveToNextMatch",
|
||||
"shift-n": "vim::MoveToPrevMatch",
|
||||
"%": "vim::Matching",
|
||||
"f": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindForward": {
|
||||
"before": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"t": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindForward": {
|
||||
"before": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"shift-f": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindBackward": {
|
||||
"after": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"shift-t": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindBackward": {
|
||||
"after": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
|
||||
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
|
||||
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
|
||||
"shift-t": ["vim::PushOperator", { "FindBackward": { "after": true } }],
|
||||
"m": ["vim::PushOperator", "Mark"],
|
||||
"'": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Jump": {
|
||||
"line": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"`": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Jump": {
|
||||
"line": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"'": ["vim::PushOperator", { "Jump": { "line": true } }],
|
||||
"`": ["vim::PushOperator", { "Jump": { "line": false } }],
|
||||
";": "vim::RepeatFind",
|
||||
",": "vim::RepeatFindReversed",
|
||||
"ctrl-o": "pane::GoBack",
|
||||
@@ -179,90 +99,25 @@
|
||||
"g shift-n": "vim::SelectPreviousMatch",
|
||||
"g l": "vim::SelectNext",
|
||||
"g shift-l": "vim::SelectPrevious",
|
||||
"g >": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"g <": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"g >": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"g <": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"g a": "editor::SelectAllMatches",
|
||||
"g s": "outline::Toggle",
|
||||
"g shift-s": "project_symbols::Toggle",
|
||||
"g .": "editor::ToggleCodeActions", // zed specific
|
||||
"g shift-a": "editor::FindAllReferences", // zed specific
|
||||
"g space": "editor::OpenExcerpts", // zed specific
|
||||
"g *": [
|
||||
"vim::MoveToNext",
|
||||
{
|
||||
"partialWord": true
|
||||
}
|
||||
],
|
||||
"g #": [
|
||||
"vim::MoveToPrev",
|
||||
{
|
||||
"partialWord": true
|
||||
}
|
||||
],
|
||||
"g j": [
|
||||
"vim::Down",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g down": [
|
||||
"vim::Down",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g k": [
|
||||
"vim::Up",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g up": [
|
||||
"vim::Up",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g $": [
|
||||
"vim::EndOfLine",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g end": [
|
||||
"vim::EndOfLine",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g 0": [
|
||||
"vim::StartOfLine",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g home": [
|
||||
"vim::StartOfLine",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g ^": [
|
||||
"vim::FirstNonWhitespace",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g *": ["vim::MoveToNext", { "partialWord": true }],
|
||||
"g #": ["vim::MoveToPrev", { "partialWord": true }],
|
||||
"g j": ["vim::Down", { "displayLines": true }],
|
||||
"g down": ["vim::Down", { "displayLines": true }],
|
||||
"g k": ["vim::Up", { "displayLines": true }],
|
||||
"g up": ["vim::Up", { "displayLines": true }],
|
||||
"g $": ["vim::EndOfLine", { "displayLines": true }],
|
||||
"g end": ["vim::EndOfLine", { "displayLines": true }],
|
||||
"g 0": ["vim::StartOfLine", { "displayLines": true }],
|
||||
"g home": ["vim::StartOfLine", { "displayLines": true }],
|
||||
"g ^": ["vim::FirstNonWhitespace", { "displayLines": true }],
|
||||
"g v": "vim::RestoreVisualSelection",
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
@@ -280,18 +135,8 @@
|
||||
"z c": "editor::Fold",
|
||||
"z o": "editor::UnfoldLines",
|
||||
"z f": "editor::FoldSelectedRanges",
|
||||
"shift-z shift-q": [
|
||||
"pane::CloseActiveItem",
|
||||
{
|
||||
"saveIntent": "skip"
|
||||
}
|
||||
],
|
||||
"shift-z shift-z": [
|
||||
"pane::CloseActiveItem",
|
||||
{
|
||||
"saveIntent": "saveAll"
|
||||
}
|
||||
],
|
||||
"shift-z shift-q": ["pane::CloseActiveItem", { "saveIntent": "skip" }],
|
||||
"shift-z shift-z": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
|
||||
// Count support
|
||||
"1": ["vim::Number", 1],
|
||||
"2": ["vim::Number", 2],
|
||||
@@ -303,6 +148,7 @@
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9],
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
@@ -386,14 +232,9 @@
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"p": "vim::Paste",
|
||||
"shift-p": [
|
||||
"vim::Paste",
|
||||
{
|
||||
"before": true
|
||||
}
|
||||
],
|
||||
"u": "editor::Undo",
|
||||
"ctrl-r": "editor::Redo",
|
||||
"shift-p": ["vim::Paste", { "before": true }],
|
||||
"u": "vim::Undo",
|
||||
"ctrl-r": "vim::Redo",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
@@ -442,12 +283,7 @@
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == c",
|
||||
"bindings": {
|
||||
"s": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"ChangeSurrounds": {}
|
||||
}
|
||||
]
|
||||
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -492,12 +328,7 @@
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == y",
|
||||
"bindings": {
|
||||
"s": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"AddSurrounds": {}
|
||||
}
|
||||
]
|
||||
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -522,12 +353,7 @@
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": [
|
||||
"vim::Word",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
|
||||
"t": "vim::Tag",
|
||||
"s": "vim::Sentence",
|
||||
"p": "vim::Paragraph",
|
||||
@@ -562,43 +388,18 @@
|
||||
"y": "vim::VisualYank",
|
||||
"shift-y": "vim::VisualYank",
|
||||
"p": "vim::Paste",
|
||||
"shift-p": [
|
||||
"vim::Paste",
|
||||
{
|
||||
"preserveClipboard": true
|
||||
}
|
||||
],
|
||||
"shift-p": ["vim::Paste", { "preserveClipboard": true }],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"shift-r": "vim::SubstituteLine",
|
||||
"c": "vim::Substitute",
|
||||
"~": "vim::ChangeCase",
|
||||
"*": [
|
||||
"vim::MoveToNext",
|
||||
{
|
||||
"partialWord": true
|
||||
}
|
||||
],
|
||||
"#": [
|
||||
"vim::MoveToPrev",
|
||||
{
|
||||
"partialWord": true
|
||||
}
|
||||
],
|
||||
"*": ["vim::MoveToNext", { "partialWord": true }],
|
||||
"#": ["vim::MoveToPrev", { "partialWord": true }],
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"g ctrl-a": [
|
||||
"vim::Increment",
|
||||
{
|
||||
"step": true
|
||||
}
|
||||
],
|
||||
"g ctrl-x": [
|
||||
"vim::Decrement",
|
||||
{
|
||||
"step": true
|
||||
}
|
||||
],
|
||||
"g ctrl-a": ["vim::Increment", { "step": true }],
|
||||
"g ctrl-x": ["vim::Decrement", { "step": true }],
|
||||
"shift-i": "vim::InsertBefore",
|
||||
"shift-a": "vim::InsertAfter",
|
||||
"shift-j": "vim::JoinLines",
|
||||
@@ -608,22 +409,8 @@
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"],
|
||||
">": "vim::Indent",
|
||||
"<": "vim::Outdent",
|
||||
"i": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"a": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
]
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -644,6 +431,7 @@
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"ctrl-x": null,
|
||||
"ctrl-x ctrl-o": "editor::ShowCompletions",
|
||||
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
|
||||
"ctrl-x ctrl-c": "editor::ShowInlineCompletion", // zed specific
|
||||
@@ -717,7 +505,7 @@
|
||||
"t": "project_panel::OpenPermanent",
|
||||
"v": "project_panel::OpenPermanent",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFinder",
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent"
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSON": ["flake.lock"],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json"]
|
||||
},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
@@ -775,7 +776,8 @@
|
||||
"PHP": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-php"]
|
||||
"plugins": ["@prettier/plugin-php"],
|
||||
"parser": "php"
|
||||
}
|
||||
},
|
||||
"Ruby": {
|
||||
|
||||
@@ -15,25 +15,25 @@
|
||||
"elevated_surface.background": "#2f343eff",
|
||||
"surface.background": "#2f343eff",
|
||||
"background": "#3b414dff",
|
||||
"element.background": "#2f343eff",
|
||||
"element.background": "#2e343eff",
|
||||
"element.hover": "#363c46ff",
|
||||
"element.active": "#454a56ff",
|
||||
"element.selected": "#454a56ff",
|
||||
"element.disabled": "#2f343eff",
|
||||
"element.disabled": "#2e343eff",
|
||||
"drop_target.background": "#83899480",
|
||||
"ghost_element.background": "#00000000",
|
||||
"ghost_element.hover": "#363c46ff",
|
||||
"ghost_element.active": "#454a56ff",
|
||||
"ghost_element.selected": "#454a56ff",
|
||||
"ghost_element.disabled": "#2f343eff",
|
||||
"ghost_element.disabled": "#2e343eff",
|
||||
"text": "#c8ccd4ff",
|
||||
"text.muted": "#838994ff",
|
||||
"text.placeholder": "#555a63ff",
|
||||
"text.disabled": "#555a63ff",
|
||||
"text.placeholder": "#696B77ff",
|
||||
"text.disabled": "#696B77ff",
|
||||
"text.accent": "#74ade8ff",
|
||||
"icon": "#c8ccd4ff",
|
||||
"icon.muted": "#838994ff",
|
||||
"icon.disabled": "#555a63ff",
|
||||
"icon.disabled": "#696B77ff",
|
||||
"icon.placeholder": "#838994ff",
|
||||
"icon.accent": "#74ade8ff",
|
||||
"status_bar.background": "#3b414dff",
|
||||
@@ -59,7 +59,7 @@
|
||||
"editor.highlighted_line.background": "#2f343eff",
|
||||
"editor.line_number": "#c8ccd459",
|
||||
"editor.active_line_number": "#c8ccd4ff",
|
||||
"editor.invisible": "#555a63ff",
|
||||
"editor.invisible": "#696B77ff",
|
||||
"editor.wrap_guide": "#c8ccd40d",
|
||||
"editor.active_wrap_guide": "#c8ccd41a",
|
||||
"editor.document_highlight.read_background": "#74ade81a",
|
||||
@@ -94,46 +94,46 @@
|
||||
"terminal.ansi.dim_white": "#575d65ff",
|
||||
"link_text.hover": "#74ade8ff",
|
||||
"conflict": "#dec184ff",
|
||||
"conflict.background": "#41321dff",
|
||||
"conflict.background": "#dec1841a",
|
||||
"conflict.border": "#5d4c2fff",
|
||||
"created": "#a1c181ff",
|
||||
"created.background": "#222e1dff",
|
||||
"created.background": "#a1c1811a",
|
||||
"created.border": "#38482fff",
|
||||
"deleted": "#d07277ff",
|
||||
"deleted.background": "#301b1bff",
|
||||
"deleted.background": "#d072771a",
|
||||
"deleted.border": "#4c2b2cff",
|
||||
"error": "#d07277ff",
|
||||
"error.background": "#301b1bff",
|
||||
"error.background": "#d072771a",
|
||||
"error.border": "#4c2b2cff",
|
||||
"hidden": "#555a63ff",
|
||||
"hidden.background": "#3b414dff",
|
||||
"hidden": "#696B77ff",
|
||||
"hidden.background": "#696B771a",
|
||||
"hidden.border": "#414754ff",
|
||||
"hint": "#5a6f89ff",
|
||||
"hint.background": "#18243dff",
|
||||
"hint.background": "#5a6f891a",
|
||||
"hint.border": "#293b5bff",
|
||||
"ignored": "#555a63ff",
|
||||
"ignored.background": "#3b414dff",
|
||||
"ignored": "#696B77ff",
|
||||
"ignored.background": "#696B771a",
|
||||
"ignored.border": "#464b57ff",
|
||||
"info": "#74ade8ff",
|
||||
"info.background": "#18243dff",
|
||||
"info.background": "#74ade81a",
|
||||
"info.border": "#293b5bff",
|
||||
"modified": "#dec184ff",
|
||||
"modified.background": "#41321dff",
|
||||
"modified.background": "#dec1841a",
|
||||
"modified.border": "#5d4c2fff",
|
||||
"predictive": "#5a6a87ff",
|
||||
"predictive.background": "#222e1dff",
|
||||
"predictive.background": "#5a6a871a",
|
||||
"predictive.border": "#38482fff",
|
||||
"renamed": "#74ade8ff",
|
||||
"renamed.background": "#18243dff",
|
||||
"renamed.background": "#74ade81a",
|
||||
"renamed.border": "#293b5bff",
|
||||
"success": "#a1c181ff",
|
||||
"success.background": "#222e1dff",
|
||||
"success.background": "#a1c1811a",
|
||||
"success.border": "#38482fff",
|
||||
"unreachable": "#838994ff",
|
||||
"unreachable.background": "#3b414dff",
|
||||
"unreachable.background": "#8389941a",
|
||||
"unreachable.border": "#464b57ff",
|
||||
"warning": "#dec184ff",
|
||||
"warning.background": "#41321dff",
|
||||
"warning.background": "#dec1841a",
|
||||
"warning.border": "#5d4c2fff",
|
||||
"players": [
|
||||
{
|
||||
@@ -491,7 +491,7 @@
|
||||
"info": "#5c78e2ff",
|
||||
"info.background": "#e2e2faff",
|
||||
"info.border": "#cbcdf6ff",
|
||||
"modified": "#dec184ff",
|
||||
"modified": "#a47a23ff",
|
||||
"modified.background": "#faf2e6ff",
|
||||
"modified.border": "#f4e7d1ff",
|
||||
"predictive": "#9b9ec6ff",
|
||||
|
||||
@@ -12,6 +12,14 @@ workspace = true
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"editor/test-support",
|
||||
"language/test-support",
|
||||
"project/test-support",
|
||||
"text/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
@@ -21,6 +29,7 @@ breadcrumbs.workspace = true
|
||||
cargo_toml.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
@@ -72,7 +81,9 @@ picker.workspace = true
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
log.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod completion_provider;
|
||||
mod context_store;
|
||||
mod context;
|
||||
pub mod context_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
@@ -16,8 +17,9 @@ use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaMo
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
pub(crate) use context_store::*;
|
||||
pub use completion_provider::*;
|
||||
pub use context::*;
|
||||
pub use context_store::*;
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
@@ -57,10 +59,14 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct MessageId(clock::Lamport);
|
||||
|
||||
impl MessageId {
|
||||
pub fn as_u64(self) -> u64 {
|
||||
self.0.as_u64()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -71,8 +77,26 @@ pub enum Role {
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn cycle(&mut self) {
|
||||
*self = match self {
|
||||
pub fn from_proto(role: i32) -> Role {
|
||||
match proto::LanguageModelRole::from_i32(role) {
|
||||
Some(proto::LanguageModelRole::LanguageModelUser) => Role::User,
|
||||
Some(proto::LanguageModelRole::LanguageModelAssistant) => Role::Assistant,
|
||||
Some(proto::LanguageModelRole::LanguageModelSystem) => Role::System,
|
||||
Some(proto::LanguageModelRole::LanguageModelTool) => Role::System,
|
||||
None => Role::User,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::LanguageModelRole {
|
||||
match self {
|
||||
Role::User => proto::LanguageModelRole::LanguageModelUser,
|
||||
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
|
||||
Role::System => proto::LanguageModelRole::LanguageModelSystem,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(self) -> Role {
|
||||
match self {
|
||||
Role::User => Role::Assistant,
|
||||
Role::Assistant => Role::System,
|
||||
Role::System => Role::User,
|
||||
@@ -151,11 +175,7 @@ pub struct LanguageModelRequestMessage {
|
||||
impl LanguageModelRequestMessage {
|
||||
pub fn to_proto(&self) -> proto::LanguageModelRequestMessage {
|
||||
proto::LanguageModelRequestMessage {
|
||||
role: match self.role {
|
||||
Role::User => proto::LanguageModelRole::LanguageModelUser,
|
||||
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
|
||||
Role::System => proto::LanguageModelRole::LanguageModelSystem,
|
||||
} as i32,
|
||||
role: self.role.to_proto() as i32,
|
||||
content: self.content.clone(),
|
||||
tool_calls: Vec::new(),
|
||||
tool_call_id: None,
|
||||
@@ -163,7 +183,7 @@ impl LanguageModelRequestMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct LanguageModelRequest {
|
||||
pub model: LanguageModel,
|
||||
pub messages: Vec<LanguageModelRequestMessage>,
|
||||
@@ -222,19 +242,48 @@ pub struct LanguageModelChoiceDelta {
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
status: MessageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum MessageStatus {
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum MessageStatus {
|
||||
Pending,
|
||||
Done,
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl MessageStatus {
|
||||
pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus {
|
||||
match status.variant {
|
||||
Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending,
|
||||
Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done,
|
||||
Some(proto::context_message_status::Variant::Error(error)) => {
|
||||
MessageStatus::Error(error.message.into())
|
||||
}
|
||||
None => MessageStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::ContextMessageStatus {
|
||||
match self {
|
||||
MessageStatus::Pending => proto::ContextMessageStatus {
|
||||
variant: Some(proto::context_message_status::Variant::Pending(
|
||||
proto::context_message_status::Pending {},
|
||||
)),
|
||||
},
|
||||
MessageStatus::Done => proto::ContextMessageStatus {
|
||||
variant: Some(proto::context_message_status::Variant::Done(
|
||||
proto::context_message_status::Done {},
|
||||
)),
|
||||
},
|
||||
MessageStatus::Error(message) => proto::ContextMessageStatus {
|
||||
variant: Some(proto::context_message_status::Variant::Error(
|
||||
proto::context_message_status::Error {
|
||||
message: message.to_string(),
|
||||
},
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state pertaining to the Assistant.
|
||||
#[derive(Default)]
|
||||
struct Assistant {
|
||||
@@ -287,6 +336,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||
})
|
||||
.detach();
|
||||
|
||||
context_store::init(&client);
|
||||
prompt_library::init(cx);
|
||||
completion_provider::init(client.clone(), cx);
|
||||
assistant_slash_command::init(cx);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
|
||||
pub use anthropic::Model as AnthropicModel;
|
||||
use gpui::Pixels;
|
||||
pub use ollama::Model as OllamaModel;
|
||||
@@ -15,8 +16,6 @@ use serde::{
|
||||
use settings::{Settings, SettingsSources};
|
||||
use strum::{EnumIter, IntoEnumIterator};
|
||||
|
||||
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
|
||||
pub enum CloudModel {
|
||||
Gpt3Point5Turbo,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
mod anthropic;
|
||||
mod cloud;
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod fake;
|
||||
mod ollama;
|
||||
mod open_ai;
|
||||
|
||||
pub use anthropic::*;
|
||||
pub use cloud::*;
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use fake::*;
|
||||
pub use ollama::*;
|
||||
pub use open_ai::*;
|
||||
use parking_lot::RwLock;
|
||||
use smol::lock::{Semaphore, SemaphoreGuardArc};
|
||||
|
||||
use crate::{
|
||||
assistant_settings::{AssistantProvider, AssistantSettings},
|
||||
@@ -21,8 +23,8 @@ use client::Client;
|
||||
use futures::{future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
/// Choose which model to use for openai provider.
|
||||
/// If the model is not available, try to use the first available model, or fallback to the original model.
|
||||
@@ -39,176 +41,117 @@ fn choose_openai_model(
|
||||
}
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let mut settings_version = 0;
|
||||
let provider = match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud(
|
||||
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
|
||||
),
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
)),
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
)),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => CompletionProvider::Ollama(OllamaCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
)),
|
||||
};
|
||||
cx.set_global(provider);
|
||||
let provider = create_provider_from_settings(client.clone(), 0, cx);
|
||||
cx.set_global(CompletionProvider::new(provider, Some(client)));
|
||||
|
||||
let mut settings_version = 0;
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
settings_version += 1;
|
||||
cx.update_global::<CompletionProvider, _>(|provider, cx| {
|
||||
match (&mut *provider, &AssistantSettings::get_global(cx).provider) {
|
||||
(
|
||||
CompletionProvider::OpenAi(provider),
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
);
|
||||
}
|
||||
(
|
||||
CompletionProvider::Anthropic(provider),
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
CompletionProvider::Ollama(provider),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
|
||||
provider.update(model.clone(), settings_version);
|
||||
}
|
||||
(_, AssistantProvider::ZedDotDev { model }) => {
|
||||
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
|
||||
model.clone(),
|
||||
client.clone(),
|
||||
settings_version,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
(
|
||||
_,
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
));
|
||||
}
|
||||
(
|
||||
_,
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
));
|
||||
}
|
||||
(
|
||||
_,
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::Ollama(OllamaCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
provider.update_settings(settings_version, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub enum CompletionProvider {
|
||||
OpenAi(OpenAiCompletionProvider),
|
||||
Anthropic(AnthropicCompletionProvider),
|
||||
Cloud(CloudCompletionProvider),
|
||||
#[cfg(test)]
|
||||
Fake(FakeCompletionProvider),
|
||||
Ollama(OllamaCompletionProvider),
|
||||
pub struct CompletionResponse {
|
||||
pub inner: BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>,
|
||||
_lock: SemaphoreGuardArc,
|
||||
}
|
||||
|
||||
pub trait LanguageModelCompletionProvider: Send + Sync {
|
||||
fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel>;
|
||||
fn settings_version(&self) -> usize;
|
||||
fn is_authenticated(&self) -> bool;
|
||||
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>>;
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView;
|
||||
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>>;
|
||||
fn model(&self) -> LanguageModel;
|
||||
fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>>;
|
||||
fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_COMPLETION_REQUESTS: usize = 4;
|
||||
|
||||
pub struct CompletionProvider {
|
||||
provider: Arc<RwLock<dyn LanguageModelCompletionProvider>>,
|
||||
client: Option<Arc<Client>>,
|
||||
request_limiter: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl CompletionProvider {
|
||||
pub fn new(
|
||||
provider: Arc<RwLock<dyn LanguageModelCompletionProvider>>,
|
||||
client: Option<Arc<Client>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
client,
|
||||
request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_COMPLETION_REQUESTS)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
|
||||
self.provider.read().available_models(cx)
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.provider.read().settings_version()
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.provider.read().is_authenticated()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
self.provider.read().authenticate(cx)
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
self.provider.read().authentication_prompt(cx)
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
self.provider.read().reset_credentials(cx)
|
||||
}
|
||||
|
||||
pub fn model(&self) -> LanguageModel {
|
||||
self.provider.read().model()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
self.provider.read().count_tokens(request, cx)
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> Task<CompletionResponse> {
|
||||
let rate_limiter = self.request_limiter.clone();
|
||||
let provider = self.provider.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let lock = rate_limiter.acquire_arc().await;
|
||||
let response = provider.read().complete(request);
|
||||
CompletionResponse {
|
||||
inner: response,
|
||||
_lock: lock,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Global for CompletionProvider {}
|
||||
@@ -218,121 +161,213 @@ impl CompletionProvider {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
pub fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider
|
||||
.available_models(cx)
|
||||
.map(LanguageModel::OpenAi)
|
||||
.collect(),
|
||||
CompletionProvider::Anthropic(provider) => provider
|
||||
.available_models()
|
||||
.map(LanguageModel::Anthropic)
|
||||
.collect(),
|
||||
CompletionProvider::Cloud(provider) => provider
|
||||
.available_models()
|
||||
.map(LanguageModel::Cloud)
|
||||
.collect(),
|
||||
CompletionProvider::Ollama(provider) => provider
|
||||
.available_models()
|
||||
.map(|model| LanguageModel::Ollama(model.clone()))
|
||||
.collect(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
pub fn update_current_as<R, T: LanguageModelCompletionProvider + 'static>(
|
||||
&mut self,
|
||||
update: impl FnOnce(&mut T) -> R,
|
||||
) -> Option<R> {
|
||||
let mut provider = self.provider.write();
|
||||
if let Some(provider) = provider.as_any_mut().downcast_mut::<T>() {
|
||||
Some(update(provider))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.settings_version(),
|
||||
CompletionProvider::Anthropic(provider) => provider.settings_version(),
|
||||
CompletionProvider::Cloud(provider) => provider.settings_version(),
|
||||
CompletionProvider::Ollama(provider) => provider.settings_version(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
pub fn update_settings(&mut self, version: usize, cx: &mut AppContext) {
|
||||
let updated = match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { model } => self
|
||||
.update_current_as::<_, CloudCompletionProvider>(|provider| {
|
||||
provider.update(model.clone(), version);
|
||||
}),
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => self.update_current_as::<_, OpenAiCompletionProvider>(|provider| {
|
||||
provider.update(
|
||||
choose_openai_model(&model, &available_models),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
version,
|
||||
);
|
||||
}),
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => self.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
version,
|
||||
);
|
||||
}),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => self.update_current_as::<_, OllamaCompletionProvider>(|provider| {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
version,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
};
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Ollama(provider) => provider.is_authenticated(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Ollama(provider) => provider.authenticate(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
|
||||
CompletionProvider::Ollama(provider) => provider.reset_credentials(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model(&self) -> LanguageModel {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
|
||||
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
|
||||
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
|
||||
CompletionProvider::Ollama(provider) => LanguageModel::Ollama(provider.model()),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => LanguageModel::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Ollama(provider) => provider.count_tokens(request, cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.complete(request),
|
||||
CompletionProvider::Anthropic(provider) => provider.complete(request),
|
||||
CompletionProvider::Cloud(provider) => provider.complete(request),
|
||||
CompletionProvider::Ollama(provider) => provider.complete(request),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(provider) => provider.complete(),
|
||||
// Previously configured provider was changed to another one
|
||||
if updated.is_none() {
|
||||
if let Some(client) = self.client.clone() {
|
||||
self.provider = create_provider_from_settings(client, version, cx);
|
||||
} else {
|
||||
log::warn!("completion provider cannot be created because client is not set");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_provider_from_settings(
|
||||
client: Arc<Client>,
|
||||
settings_version: usize,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<RwLock<dyn LanguageModelCompletionProvider>> {
|
||||
match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { model } => Arc::new(RwLock::new(
|
||||
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
|
||||
)),
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => Arc::new(RwLock::new(OpenAiCompletionProvider::new(
|
||||
choose_openai_model(&model, &available_models),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
))),
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => Arc::new(RwLock::new(AnthropicCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
))),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => Arc::new(RwLock::new(OllamaCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::AppContext;
|
||||
use parking_lot::RwLock;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
completion_provider::MAX_CONCURRENT_COMPLETION_REQUESTS, CompletionProvider,
|
||||
FakeCompletionProvider, LanguageModelRequest,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_rate_limiting(cx: &mut AppContext) {
|
||||
SettingsStore::test(cx);
|
||||
let fake_provider = FakeCompletionProvider::setup_test(cx);
|
||||
|
||||
let provider = CompletionProvider::new(Arc::new(RwLock::new(fake_provider.clone())), None);
|
||||
|
||||
// Enqueue some requests
|
||||
for i in 0..MAX_CONCURRENT_COMPLETION_REQUESTS * 2 {
|
||||
let response = provider.complete(
|
||||
LanguageModelRequest {
|
||||
temperature: i as f32 / 10.0,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let response = response.await;
|
||||
let mut stream = response.inner.await.unwrap();
|
||||
while let Some(message) = stream.next().await {
|
||||
message.unwrap();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
cx.background_executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
fake_provider.completion_count(),
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS
|
||||
);
|
||||
|
||||
// Get the first completion request that is in flight and mark it as completed.
|
||||
let completion = fake_provider
|
||||
.running_completions()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
fake_provider.finish_completion(&completion);
|
||||
|
||||
// Ensure that the number of in-flight completion requests is reduced.
|
||||
assert_eq!(
|
||||
fake_provider.completion_count(),
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS - 1
|
||||
);
|
||||
|
||||
cx.background_executor().run_until_parked();
|
||||
|
||||
// Ensure that another completion request was allowed to acquire the lock.
|
||||
assert_eq!(
|
||||
fake_provider.completion_count(),
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS
|
||||
);
|
||||
|
||||
// Mark all completion requests as finished that are in flight.
|
||||
for request in fake_provider.running_completions() {
|
||||
fake_provider.finish_completion(&request);
|
||||
}
|
||||
|
||||
assert_eq!(fake_provider.completion_count(), 0);
|
||||
|
||||
// Wait until the background tasks acquire the lock again.
|
||||
cx.background_executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
fake_provider.completion_count(),
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS - 1
|
||||
);
|
||||
|
||||
// Finish all remaining completion requests.
|
||||
for request in fake_provider.running_completions() {
|
||||
fake_provider.finish_completion(&request);
|
||||
}
|
||||
|
||||
cx.background_executor().run_until_parked();
|
||||
|
||||
assert_eq!(fake_provider.completion_count(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
|
||||
Role,
|
||||
};
|
||||
use crate::{count_open_ai_tokens, LanguageModelRequestMessage};
|
||||
use crate::{count_open_ai_tokens, LanguageModelCompletionProvider, LanguageModelRequestMessage};
|
||||
use anthropic::{stream_completion, Request, RequestMessage};
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
@@ -26,50 +26,22 @@ pub struct AnthropicCompletionProvider {
|
||||
settings_version: usize,
|
||||
}
|
||||
|
||||
impl AnthropicCompletionProvider {
|
||||
pub fn new(
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
api_key: None,
|
||||
api_url,
|
||||
model,
|
||||
http_client,
|
||||
low_speed_timeout,
|
||||
settings_version,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
) {
|
||||
self.model = model;
|
||||
self.api_url = api_url;
|
||||
self.low_speed_timeout = low_speed_timeout;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = AnthropicModel> {
|
||||
impl LanguageModelCompletionProvider for AnthropicCompletionProvider {
|
||||
fn available_models(&self, _cx: &AppContext) -> Vec<LanguageModel> {
|
||||
AnthropicModel::iter()
|
||||
.map(LanguageModel::Anthropic)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.api_key.is_some()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
if self.is_authenticated() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
@@ -85,36 +57,36 @@ impl AnthropicCompletionProvider {
|
||||
String::from_utf8(api_key)?
|
||||
};
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::Anthropic(provider) = provider {
|
||||
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
|
||||
provider.api_key = Some(api_key);
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let delete_credentials = cx.delete_credentials(&self.api_url);
|
||||
cx.spawn(|mut cx| async move {
|
||||
delete_credentials.await.log_err();
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::Anthropic(provider) = provider {
|
||||
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
|
||||
provider.api_key = None;
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn model(&self) -> AnthropicModel {
|
||||
self.model.clone()
|
||||
fn model(&self) -> LanguageModel {
|
||||
LanguageModel::Anthropic(self.model.clone())
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
@@ -122,7 +94,7 @@ impl AnthropicCompletionProvider {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
@@ -167,12 +139,48 @@ impl AnthropicCompletionProvider {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AnthropicCompletionProvider {
|
||||
pub fn new(
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
api_key: None,
|
||||
api_url,
|
||||
model,
|
||||
http_client,
|
||||
low_speed_timeout,
|
||||
settings_version,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
) {
|
||||
self.model = model;
|
||||
self.api_url = api_url;
|
||||
self.low_speed_timeout = low_speed_timeout;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
|
||||
preprocess_anthropic_request(&mut request);
|
||||
|
||||
let model = match request.model {
|
||||
LanguageModel::Anthropic(model) => model,
|
||||
_ => self.model(),
|
||||
_ => self.model.clone(),
|
||||
};
|
||||
|
||||
let mut system_message = String::new();
|
||||
@@ -278,9 +286,9 @@ impl AuthenticationPrompt {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
write_credentials.await?;
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::Anthropic(provider) = provider {
|
||||
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
|
||||
provider.api_key = Some(api_key);
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
assistant_settings::CloudModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
|
||||
LanguageModelRequest,
|
||||
LanguageModelCompletionProvider, LanguageModelRequest,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, Client};
|
||||
@@ -30,11 +30,9 @@ impl CloudCompletionProvider {
|
||||
let maintain_client_status = cx.spawn(|mut cx| async move {
|
||||
while let Some(status) = status_rx.next().await {
|
||||
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::Cloud(provider) = provider {
|
||||
provider.update_current_as::<_, Self>(|provider| {
|
||||
provider.status = status;
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -51,44 +49,53 @@ impl CloudCompletionProvider {
|
||||
self.model = model;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = CloudModel> {
|
||||
impl LanguageModelCompletionProvider for CloudCompletionProvider {
|
||||
fn available_models(&self, _cx: &AppContext) -> Vec<LanguageModel> {
|
||||
let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() {
|
||||
Some(custom_model)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
CloudModel::iter().filter_map(move |model| {
|
||||
if let CloudModel::Custom(_) = model {
|
||||
Some(CloudModel::Custom(custom_model.take()?))
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
})
|
||||
CloudModel::iter()
|
||||
.filter_map(move |model| {
|
||||
if let CloudModel::Custom(_) = model {
|
||||
Some(CloudModel::Custom(custom_model.take()?))
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
})
|
||||
.map(LanguageModel::Cloud)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn model(&self) -> CloudModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.status.is_connected()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |cx| async move { client.authenticate_and_connect(true, &cx).await })
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|_cx| AuthenticationPrompt).into()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
fn reset_credentials(&self, _cx: &AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn model(&self) -> LanguageModel {
|
||||
LanguageModel::Cloud(self.model.clone())
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
@@ -128,7 +135,7 @@ impl CloudCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
fn complete(
|
||||
&self,
|
||||
mut request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
@@ -161,6 +168,10 @@ impl CloudCompletionProvider {
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt;
|
||||
|
||||
@@ -1,29 +1,106 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, Task};
|
||||
use std::sync::Arc;
|
||||
use ui::WindowContext;
|
||||
|
||||
use crate::{LanguageModel, LanguageModelCompletionProvider, LanguageModelRequest};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FakeCompletionProvider {
|
||||
current_completion_tx: Arc<parking_lot::Mutex<Option<mpsc::UnboundedSender<String>>>>,
|
||||
current_completion_txs: Arc<parking_lot::Mutex<HashMap<String, mpsc::UnboundedSender<String>>>>,
|
||||
}
|
||||
|
||||
impl FakeCompletionProvider {
|
||||
pub fn complete(&self) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
*self.current_completion_tx.lock() = Some(tx);
|
||||
async move { Ok(rx.map(Ok).boxed()) }.boxed()
|
||||
pub fn setup_test(cx: &mut AppContext) -> Self {
|
||||
use crate::CompletionProvider;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
let this = Self::default();
|
||||
let provider = CompletionProvider::new(Arc::new(RwLock::new(this.clone())), None);
|
||||
cx.set_global(provider);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn send_completion(&self, chunk: String) {
|
||||
self.current_completion_tx
|
||||
pub fn running_completions(&self) -> Vec<LanguageModelRequest> {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.as_ref()
|
||||
.keys()
|
||||
.map(|k| serde_json::from_str(k).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn completion_count(&self) -> usize {
|
||||
self.current_completion_txs.lock().len()
|
||||
}
|
||||
|
||||
pub fn send_completion(&self, request: &LanguageModelRequest, chunk: String) {
|
||||
let json = serde_json::to_string(request).unwrap();
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.get(&json)
|
||||
.unwrap()
|
||||
.unbounded_send(chunk)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn finish_completion(&self) {
|
||||
self.current_completion_tx.lock().take();
|
||||
pub fn finish_completion(&self, request: &LanguageModelRequest) {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.remove(&serde_json::to_string(request).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelCompletionProvider for FakeCompletionProvider {
|
||||
fn available_models(&self, _cx: &AppContext) -> Vec<LanguageModel> {
|
||||
vec![LanguageModel::default()]
|
||||
}
|
||||
|
||||
fn settings_version(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn authenticate(&self, _cx: &AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, _cx: &mut WindowContext) -> AnyView {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, _cx: &AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn model(&self) -> LanguageModel {
|
||||
LanguageModel::default()
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
&self,
|
||||
_request: LanguageModelRequest,
|
||||
_cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
futures::future::ready(Ok(0)).boxed()
|
||||
}
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
_request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.insert(serde_json::to_string(&_request).unwrap(), tx);
|
||||
async move { Ok(rx.map(Ok).boxed()) }.boxed()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::LanguageModelCompletionProvider;
|
||||
use crate::{
|
||||
assistant_settings::OllamaModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
@@ -26,6 +27,108 @@ pub struct OllamaCompletionProvider {
|
||||
available_models: Vec<OllamaModel>,
|
||||
}
|
||||
|
||||
impl LanguageModelCompletionProvider for OllamaCompletionProvider {
|
||||
fn available_models(&self, _cx: &AppContext) -> Vec<LanguageModel> {
|
||||
self.available_models
|
||||
.iter()
|
||||
.map(|m| LanguageModel::Ollama(m.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
!self.available_models.is_empty()
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
if self.is_authenticated() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
self.fetch_models(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
let fetch_models = Box::new(move |cx: &mut WindowContext| {
|
||||
cx.update_global::<CompletionProvider, _>(|provider, cx| {
|
||||
provider
|
||||
.update_current_as::<_, OllamaCompletionProvider>(|provider| {
|
||||
provider.fetch_models(cx)
|
||||
})
|
||||
.unwrap_or_else(|| Task::ready(Ok(())))
|
||||
})
|
||||
});
|
||||
|
||||
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
self.fetch_models(cx)
|
||||
}
|
||||
|
||||
fn model(&self) -> LanguageModel {
|
||||
LanguageModel::Ollama(self.model.clone())
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
_cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
// There is no endpoint for this _yet_ in Ollama
|
||||
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
|
||||
let token_count = request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| msg.content.chars().count())
|
||||
.sum::<usize>()
|
||||
/ 4;
|
||||
|
||||
async move { Ok(token_count) }.boxed()
|
||||
}
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = self.to_ollama_request(request);
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
let low_speed_timeout = self.low_speed_timeout;
|
||||
async move {
|
||||
let request =
|
||||
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(delta) => {
|
||||
let content = match delta.message {
|
||||
ChatMessage::User { content } => content,
|
||||
ChatMessage::Assistant { content } => content,
|
||||
ChatMessage::System { content } => content,
|
||||
};
|
||||
Some(Ok(content))
|
||||
}
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl OllamaCompletionProvider {
|
||||
pub fn new(
|
||||
model: OllamaModel,
|
||||
@@ -87,36 +190,12 @@ impl OllamaCompletionProvider {
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = &OllamaModel> {
|
||||
self.available_models.iter()
|
||||
}
|
||||
|
||||
pub fn select_first_available_model(&mut self) {
|
||||
if let Some(model) = self.available_models.first() {
|
||||
self.model = model.clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
!self.available_models.is_empty()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
if self.is_authenticated() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
self.fetch_models(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
self.fetch_models(cx)
|
||||
}
|
||||
|
||||
pub fn fetch_models(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let http_client = self.http_client.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
@@ -137,90 +216,21 @@ impl OllamaCompletionProvider {
|
||||
models.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::Ollama(provider) = provider {
|
||||
provider.update_current_as::<_, OllamaCompletionProvider>(|provider| {
|
||||
provider.available_models = models;
|
||||
|
||||
if !provider.available_models.is_empty() && provider.model.name.is_empty() {
|
||||
provider.select_first_available_model()
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
let fetch_models = Box::new(move |cx: &mut WindowContext| {
|
||||
cx.update_global::<CompletionProvider, _>(|provider, cx| {
|
||||
if let CompletionProvider::Ollama(provider) = provider {
|
||||
provider.fetch_models(cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn model(&self) -> OllamaModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
_cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
// There is no endpoint for this _yet_ in Ollama
|
||||
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
|
||||
let token_count = request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| msg.content.chars().count())
|
||||
.sum::<usize>()
|
||||
/ 4;
|
||||
|
||||
async move { Ok(token_count) }.boxed()
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = self.to_ollama_request(request);
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
let low_speed_timeout = self.low_speed_timeout;
|
||||
async move {
|
||||
let request =
|
||||
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(delta) => {
|
||||
let content = match delta.message {
|
||||
ChatMessage::User { content } => content,
|
||||
ChatMessage::Assistant { content } => content,
|
||||
ChatMessage::System { content } => content,
|
||||
};
|
||||
Some(Ok(content))
|
||||
}
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
|
||||
let model = match request.model {
|
||||
LanguageModel::Ollama(model) => model,
|
||||
_ => self.model(),
|
||||
_ => self.model.clone(),
|
||||
};
|
||||
|
||||
ChatRequest {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::assistant_settings::CloudModel;
|
||||
use crate::assistant_settings::{AssistantProvider, AssistantSettings};
|
||||
use crate::LanguageModelCompletionProvider;
|
||||
use crate::{
|
||||
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
@@ -57,37 +58,75 @@ impl OpenAiCompletionProvider {
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self, cx: &AppContext) -> impl Iterator<Item = OpenAiModel> {
|
||||
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
|
||||
let model = match request.model {
|
||||
LanguageModel::OpenAi(model) => model,
|
||||
_ => self.model.clone(),
|
||||
};
|
||||
|
||||
Request {
|
||||
model,
|
||||
messages: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| match msg.role {
|
||||
Role::User => RequestMessage::User {
|
||||
content: msg.content,
|
||||
},
|
||||
Role::Assistant => RequestMessage::Assistant {
|
||||
content: Some(msg.content),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => RequestMessage::System {
|
||||
content: msg.content,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
stream: true,
|
||||
stop: request.stop,
|
||||
temperature: request.temperature,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelCompletionProvider for OpenAiCompletionProvider {
|
||||
fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
|
||||
if let AssistantProvider::OpenAi {
|
||||
available_models, ..
|
||||
} = &AssistantSettings::get_global(cx).provider
|
||||
{
|
||||
if !available_models.is_empty() {
|
||||
// available_models is set, just return it
|
||||
return available_models.clone().into_iter();
|
||||
return available_models
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(LanguageModel::OpenAi)
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
let available_models = if matches!(self.model, OpenAiModel::Custom { .. }) {
|
||||
// available_models is not set but the default model is set to custom, only show custom
|
||||
vec![self.model.clone()]
|
||||
} else {
|
||||
// default case, use all models except custom
|
||||
OpenAiModel::iter()
|
||||
.filter(|model| !matches!(model, OpenAiModel::Custom { .. }))
|
||||
.collect()
|
||||
};
|
||||
available_models.into_iter()
|
||||
available_models
|
||||
.into_iter()
|
||||
.map(LanguageModel::OpenAi)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.api_key.is_some()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
if self.is_authenticated() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
@@ -103,36 +142,36 @@ impl OpenAiCompletionProvider {
|
||||
String::from_utf8(api_key)?
|
||||
};
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::OpenAi(provider) = provider {
|
||||
provider.update_current_as::<_, Self>(|provider| {
|
||||
provider.api_key = Some(api_key);
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let delete_credentials = cx.delete_credentials(&self.api_url);
|
||||
cx.spawn(|mut cx| async move {
|
||||
delete_credentials.await.log_err();
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::OpenAi(provider) = provider {
|
||||
provider.update_current_as::<_, Self>(|provider| {
|
||||
provider.api_key = None;
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn model(&self) -> OpenAiModel {
|
||||
self.model.clone()
|
||||
fn model(&self) -> LanguageModel {
|
||||
LanguageModel::OpenAi(self.model.clone())
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
@@ -140,7 +179,7 @@ impl OpenAiCompletionProvider {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
@@ -173,36 +212,8 @@ impl OpenAiCompletionProvider {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
|
||||
let model = match request.model {
|
||||
LanguageModel::OpenAi(model) => model,
|
||||
_ => self.model(),
|
||||
};
|
||||
|
||||
Request {
|
||||
model,
|
||||
messages: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| match msg.role {
|
||||
Role::User => RequestMessage::User {
|
||||
content: msg.content,
|
||||
},
|
||||
Role::Assistant => RequestMessage::Assistant {
|
||||
content: Some(msg.content),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => RequestMessage::System {
|
||||
content: msg.content,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
stream: true,
|
||||
stop: request.stop,
|
||||
temperature: request.temperature,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
}
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,9 +295,9 @@ impl AuthenticationPrompt {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
write_credentials.await?;
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::OpenAi(provider) = provider {
|
||||
provider.update_current_as::<_, OpenAiCompletionProvider>(|provider| {
|
||||
provider.api_key = Some(api_key);
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
3009
crates/assistant/src/context.rs
Normal file
3009
crates/assistant/src/context.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,97 +1,117 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use collections::HashMap;
|
||||
use crate::{
|
||||
Context, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||
use clock::ReplicaId;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Model, ModelContext, Task};
|
||||
use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
use paths::contexts_dir;
|
||||
use project::Project;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use ui::Context;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContext {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
pub slash_command_output_sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
}
|
||||
|
||||
impl SavedContext {
|
||||
pub const VERSION: &'static str = "0.3.0";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContextV0_2_0 {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedContextV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
|
||||
client.add_model_request_handler(ContextStore::handle_open_context);
|
||||
client.add_model_message_handler(ContextStore::handle_update_context);
|
||||
client.add_model_request_handler(ContextStore::handle_synchronize_contexts);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
pub struct RemoteContextMetadata {
|
||||
pub id: ContextId,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ContextStore {
|
||||
contexts: Vec<ContextHandle>,
|
||||
contexts_metadata: Vec<SavedContextMetadata>,
|
||||
host_contexts: Vec<RemoteContextMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
client: Arc<Client>,
|
||||
project: Model<Project>,
|
||||
project_is_shared: bool,
|
||||
client_subscription: Option<client::Subscription>,
|
||||
_project_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
enum ContextHandle {
|
||||
Weak(WeakModel<Context>),
|
||||
Strong(Model<Context>),
|
||||
}
|
||||
|
||||
impl ContextHandle {
|
||||
fn upgrade(&self) -> Option<Model<Context>> {
|
||||
match self {
|
||||
ContextHandle::Weak(weak) => weak.upgrade(),
|
||||
ContextHandle::Strong(strong) => Some(strong.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakModel<Context> {
|
||||
match self {
|
||||
ContextHandle::Weak(weak) => weak.clone(),
|
||||
ContextHandle::Strong(strong) => strong.downgrade(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
pub fn new(project: Model<Project>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let languages = project.read(cx).languages().clone();
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||
contexts_metadata: Vec::new(),
|
||||
fs,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
.log_err()
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.observe(&project, Self::handle_project_changed),
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
};
|
||||
this.handle_project_changed(project, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
@@ -100,54 +120,433 @@ impl ContextStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
|
||||
async fn handle_advertise_contexts(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::AdvertiseContexts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.host_contexts = envelope
|
||||
.payload
|
||||
.contexts
|
||||
.into_iter()
|
||||
.map(|context| RemoteContextMetadata {
|
||||
id: ContextId::from_proto(context.context_id),
|
||||
summary: context.summary,
|
||||
})
|
||||
.collect();
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_open_context(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::OpenContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::OpenContextResponse> {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
let operations = this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_remote() {
|
||||
return Err(anyhow!("only the host contexts can be opened"));
|
||||
}
|
||||
|
||||
let context = this
|
||||
.loaded_context_for_id(&context_id, cx)
|
||||
.context("context not found")?;
|
||||
if context.read(cx).replica_id() != ReplicaId::default() {
|
||||
return Err(anyhow!("context must be opened via the host"));
|
||||
}
|
||||
|
||||
anyhow::Ok(
|
||||
context
|
||||
.read(cx)
|
||||
.serialize_ops(&ContextVersion::default(), cx),
|
||||
)
|
||||
})??;
|
||||
let operations = operations.await;
|
||||
Ok(proto::OpenContextResponse {
|
||||
context: Some(proto::Context { operations }),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_update_context(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
let operation_proto = envelope.payload.operation.context("invalid operation")?;
|
||||
let operation = ContextOperation::from_proto(operation_proto)?;
|
||||
context.update(cx, |context, cx| context.apply_ops([operation], cx))?;
|
||||
}
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
|
||||
async fn handle_synchronize_contexts(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::SynchronizeContexts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::SynchronizeContextsResponse> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_remote() {
|
||||
return Err(anyhow!("only the host can synchronize contexts"));
|
||||
}
|
||||
|
||||
let mut local_versions = Vec::new();
|
||||
for remote_version_proto in envelope.payload.contexts {
|
||||
let remote_version = ContextVersion::from_proto(&remote_version_proto);
|
||||
let context_id = ContextId::from_proto(remote_version_proto.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
let context = context.read(cx);
|
||||
let operations = context.serialize_ops(&remote_version, cx);
|
||||
local_versions.push(context.version(cx).to_proto(context_id.clone()));
|
||||
let client = this.client.clone();
|
||||
let project_id = envelope.payload.project_id;
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let operations = operations.await;
|
||||
for operation in operations {
|
||||
client.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
operation: Some(operation),
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
this.advertise_contexts(cx);
|
||||
|
||||
anyhow::Ok(proto::SynchronizeContextsResponse {
|
||||
contexts: local_versions,
|
||||
})
|
||||
})?
|
||||
}
|
||||
|
||||
fn handle_project_changed(&mut self, _: Model<Project>, cx: &mut ModelContext<Self>) {
|
||||
let is_shared = self.project.read(cx).is_shared();
|
||||
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
|
||||
if is_shared == was_shared {
|
||||
return;
|
||||
}
|
||||
|
||||
if is_shared {
|
||||
self.contexts.retain_mut(|context| {
|
||||
if let Some(strong_context) = context.upgrade() {
|
||||
*context = ContextHandle::Strong(strong_context);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
let remote_id = self.project.read(cx).remote_id().unwrap();
|
||||
self.client_subscription = self
|
||||
.client
|
||||
.subscribe_to_entity(remote_id)
|
||||
.log_err()
|
||||
.map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async()));
|
||||
self.advertise_contexts(cx);
|
||||
} else {
|
||||
self.client_subscription = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_: Model<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::Reshared => {
|
||||
self.advertise_contexts(cx);
|
||||
}
|
||||
project::Event::HostReshared | project::Event::Rejoined => {
|
||||
self.synchronize_contexts(cx);
|
||||
}
|
||||
project::Event::DisconnectedFromHost => {
|
||||
self.contexts.retain_mut(|context| {
|
||||
if let Some(strong_context) = context.upgrade() {
|
||||
*context = ContextHandle::Weak(context.downgrade());
|
||||
strong_context.update(cx, |context, cx| {
|
||||
if context.replica_id() != ReplicaId::default() {
|
||||
context.set_capability(language::Capability::ReadOnly, cx);
|
||||
}
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
self.host_contexts.clear();
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(self.languages.clone(), Some(self.telemetry.clone()), cx)
|
||||
});
|
||||
self.register_context(&context, cx);
|
||||
context
|
||||
}
|
||||
|
||||
pub fn open_local_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
|
||||
return Task::ready(Ok(existing_context));
|
||||
}
|
||||
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let saved_context = fs.load(&path).await?;
|
||||
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
|
||||
match saved_context_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
SavedContext::VERSION => {
|
||||
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
|
||||
}
|
||||
"0.2.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_2_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
slash_command_output_sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
"0.1.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
slash_command_output_sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved context")),
|
||||
let languages = self.languages.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let load = cx.background_executor().spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let saved_context = fs.load(&path).await?;
|
||||
SavedContext::from_json(&saved_context)
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let saved_context = load.await?;
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::deserialize(saved_context, path.clone(), languages, Some(telemetry), cx)
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn loaded_context_for_path(&self, path: &Path, cx: &AppContext) -> Option<Model<Context>> {
|
||||
self.contexts.iter().find_map(|context| {
|
||||
let context = context.upgrade()?;
|
||||
if context.read(cx).path() == Some(path) {
|
||||
Some(context)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn loaded_context_for_id(&self, id: &ContextId, cx: &AppContext) -> Option<Model<Context>> {
|
||||
self.contexts.iter().find_map(|context| {
|
||||
let context = context.upgrade()?;
|
||||
if context.read(cx).id() == id {
|
||||
Some(context)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_remote_context(
|
||||
&mut self,
|
||||
context_id: ContextId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow!("project was not remote")));
|
||||
};
|
||||
if project.is_local() {
|
||||
return Task::ready(Err(anyhow!("cannot open remote contexts as the host")));
|
||||
}
|
||||
|
||||
if let Some(context) = self.loaded_context_for_id(&context_id, cx) {
|
||||
return Task::ready(Ok(context));
|
||||
}
|
||||
|
||||
let replica_id = project.replica_id();
|
||||
let capability = project.capability();
|
||||
let language_registry = self.languages.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let request = self.client.request(proto::OpenContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
replica_id,
|
||||
capability,
|
||||
language_registry,
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
let operations = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
context_proto
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(|op| ContextOperation::from_proto(op))
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.await?;
|
||||
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))??;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn register_context(&mut self, context: &Model<Context>, cx: &mut ModelContext<Self>) {
|
||||
let handle = if self.project_is_shared {
|
||||
ContextHandle::Strong(context.clone())
|
||||
} else {
|
||||
ContextHandle::Weak(context.downgrade())
|
||||
};
|
||||
self.contexts.push(handle);
|
||||
self.advertise_contexts(cx);
|
||||
cx.subscribe(context, Self::handle_context_event).detach();
|
||||
}
|
||||
|
||||
fn handle_context_event(
|
||||
&mut self,
|
||||
context: Model<Context>,
|
||||
event: &ContextEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
ContextEvent::SummaryChanged => {
|
||||
self.advertise_contexts(cx);
|
||||
}
|
||||
ContextEvent::Operation(operation) => {
|
||||
let context_id = context.read(cx).id().to_proto();
|
||||
let operation = operation.to_proto();
|
||||
self.client
|
||||
.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id,
|
||||
operation: Some(operation),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn advertise_contexts(&self, cx: &AppContext) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// For now, only the host can advertise their open contexts.
|
||||
if self.project.read(cx).is_remote() {
|
||||
return;
|
||||
}
|
||||
|
||||
let contexts = self
|
||||
.contexts
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|context| {
|
||||
let context = context.upgrade()?.read(cx);
|
||||
if context.replica_id() == ReplicaId::default() {
|
||||
Some(proto::ContextMetadata {
|
||||
context_id: context.id().to_proto(),
|
||||
summary: context.summary().map(|summary| summary.text.clone()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
self.client
|
||||
.send(proto::AdvertiseContexts {
|
||||
project_id,
|
||||
contexts,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn synchronize_contexts(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let contexts = self
|
||||
.contexts
|
||||
.iter()
|
||||
.filter_map(|context| {
|
||||
let context = context.upgrade()?.read(cx);
|
||||
if context.replica_id() != ReplicaId::default() {
|
||||
Some(context.version(cx).to_proto(context.id().clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let client = self.client.clone();
|
||||
let request = self.client.request(proto::SynchronizeContexts {
|
||||
project_id,
|
||||
contexts,
|
||||
});
|
||||
cx.spawn(|this, cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
let mut context_ids = Vec::new();
|
||||
let mut operations = Vec::new();
|
||||
this.read_with(&cx, |this, cx| {
|
||||
for context_version_proto in response.contexts {
|
||||
let context_version = ContextVersion::from_proto(&context_version_proto);
|
||||
let context_id = ContextId::from_proto(context_version_proto.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
context_ids.push(context_id);
|
||||
operations.push(context.read(cx).serialize_ops(&context_version, cx));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let operations = futures::future::join_all(operations).await;
|
||||
for (context_id, operations) in context_ids.into_iter().zip(operations) {
|
||||
for operation in operations {
|
||||
client.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
operation: Some(operation),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
|
||||
let metadata = self.contexts_metadata.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
@@ -178,6 +577,10 @@ impl ContextStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn host_contexts(&self) -> &[RemoteContextMetadata] {
|
||||
&self.host_contexts
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
||||
@@ -1986,13 +1986,14 @@ impl Codegen {
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
|
||||
|
||||
let model_telemetry_id = prompt.model.telemetry_id();
|
||||
let response = CompletionProvider::global(cx).complete(prompt);
|
||||
let response = CompletionProvider::global(cx).complete(prompt, cx);
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.edit_position = range.start;
|
||||
self.diff = Diff::default();
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = response.await;
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
@@ -2002,7 +2003,7 @@ impl Codegen {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = StripInvalidSpans::new(response.await?);
|
||||
let chunks = StripInvalidSpans::new(response.inner.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
@@ -2473,9 +2474,8 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
@@ -2495,8 +2495,11 @@ mod tests {
|
||||
});
|
||||
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx));
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.start(LanguageModelRequest::default(), cx)
|
||||
});
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
@@ -2508,11 +2511,11 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
provider.finish_completion(&LanguageModelRequest::default());
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
@@ -2533,8 +2536,7 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
@@ -2555,6 +2557,8 @@ mod tests {
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
@@ -2565,11 +2569,11 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
provider.finish_completion(&LanguageModelRequest::default());
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
@@ -2590,8 +2594,7 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
@@ -2612,6 +2615,8 @@ mod tests {
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
@@ -2622,11 +2627,11 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
provider.finish_completion(&LanguageModelRequest::default());
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -49,6 +49,7 @@ impl RenderOnce for ModelSelector {
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -67,14 +68,11 @@ impl RenderOnce for ModelSelector {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::{
|
||||
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
@@ -448,7 +447,6 @@ impl PromptLibrary {
|
||||
self.set_active_prompt(Some(prompt_id), cx);
|
||||
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
let prompt = self.store.load(prompt_id);
|
||||
self.pending_load = cx.spawn(|this, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
@@ -477,7 +475,7 @@ impl PromptLibrary {
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Box::new(
|
||||
SlashCommandCompletionProvider::new(commands, None, None),
|
||||
SlashCommandCompletionProvider::new(None, None),
|
||||
));
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
|
||||
@@ -31,7 +31,6 @@ pub mod tabs_command;
|
||||
pub mod term_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
@@ -46,14 +45,12 @@ pub(crate) struct SlashCommandLine {
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
editor,
|
||||
commands,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
@@ -65,8 +62,8 @@ impl SlashCommandCompletionProvider {
|
||||
name_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let candidates = self
|
||||
.commands
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
let candidates = commands
|
||||
.command_names()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -76,7 +73,6 @@ impl SlashCommandCompletionProvider {
|
||||
char_bag: def.as_ref().into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let commands = self.commands.clone();
|
||||
let command_name = command_name.to_string();
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -155,7 +151,8 @@ impl SlashCommandCompletionProvider {
|
||||
flag.store(true, SeqCst);
|
||||
*flag = new_cancel_flag.clone();
|
||||
|
||||
if let Some(command) = self.commands.command(command_name) {
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
if let Some(command) = commands.command(command_name) {
|
||||
let completions = command.complete_argument(
|
||||
argument,
|
||||
new_cancel_flag.clone(),
|
||||
@@ -170,7 +167,7 @@ impl SlashCommandCompletionProvider {
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|command_argument| {
|
||||
let confirm =
|
||||
let confirm = if command_argument.run_command {
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
@@ -178,7 +175,7 @@ impl SlashCommandCompletionProvider {
|
||||
Arc::new({
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
let command_argument = command_argument.clone();
|
||||
let command_argument = command_argument.new_text.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
@@ -194,15 +191,24 @@ impl SlashCommandCompletionProvider {
|
||||
.ok();
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut new_text = command_argument.new_text.clone();
|
||||
if !command_argument.run_command {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(command_argument.clone(), None),
|
||||
new_text: command_argument.clone(),
|
||||
label: CodeLabel::plain(command_argument.label, None),
|
||||
new_text,
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
show_new_completions_on_confirm: !command_argument.run_command,
|
||||
confirm,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::{
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::ArgumentCompletion;
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
@@ -33,7 +34,7 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{
|
||||
@@ -36,7 +36,7 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{create_label_for_command, SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use fuzzy::{PathMatch, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use language::{
|
||||
@@ -108,7 +108,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
@@ -143,7 +143,14 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
.map(|candidate| candidate.string),
|
||||
);
|
||||
|
||||
Ok(matches)
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|completion| ArgumentCompletion {
|
||||
label: completion.clone(),
|
||||
new_text: completion,
|
||||
run_command: true,
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use indexed_docs::{
|
||||
IndexedDocsRegistry, IndexedDocsStore, LocalProvider, PackageName, ProviderId, RustdocIndexer,
|
||||
@@ -92,7 +94,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
_cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
self.ensure_rustdoc_provider_is_registered(workspace, cx);
|
||||
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
@@ -102,15 +104,17 @@ impl SlashCommand for DocsSlashCommand {
|
||||
.ok_or_else(|| anyhow!("no docs provider specified"))
|
||||
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
|
||||
cx.background_executor().spawn(async move {
|
||||
/// HACK: Prefixes the completions with the provider ID so that it doesn't get deleted
|
||||
/// when a completion is accepted.
|
||||
///
|
||||
/// We will likely want to extend `complete_argument` with support for replacing just
|
||||
/// a particular range of the argument when a completion is accepted.
|
||||
fn prefix_with_provider(provider: ProviderId, items: Vec<String>) -> Vec<String> {
|
||||
fn build_completions(
|
||||
provider: ProviderId,
|
||||
items: Vec<String>,
|
||||
) -> Vec<ArgumentCompletion> {
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| format!("{provider} {item}"))
|
||||
.map(|item| ArgumentCompletion {
|
||||
label: item.clone(),
|
||||
new_text: format!("{provider} {item}"),
|
||||
run_command: true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -119,7 +123,11 @@ impl SlashCommand for DocsSlashCommand {
|
||||
let providers = indexed_docs_registry.list_providers();
|
||||
Ok(providers
|
||||
.into_iter()
|
||||
.map(|provider| provider.to_string())
|
||||
.map(|provider| ArgumentCompletion {
|
||||
label: provider.to_string(),
|
||||
new_text: provider.to_string(),
|
||||
run_command: false,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
@@ -136,7 +144,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
}
|
||||
|
||||
let items = store.search(package).await;
|
||||
Ok(prefix_with_provider(provider, items))
|
||||
Ok(build_completions(provider, items))
|
||||
}
|
||||
DocsSlashCommandArgs::SearchItemDocs {
|
||||
provider,
|
||||
@@ -145,7 +153,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
} => {
|
||||
let store = store?;
|
||||
let items = store.search(item_path).await;
|
||||
Ok(prefix_with_provider(provider, items))
|
||||
Ok(build_completions(provider, items))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,9 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
@@ -119,7 +121,7 @@ impl SlashCommand for FetchSlashCommand {
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{diagnostics_command::write_single_file_diagnostics, SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
|
||||
@@ -105,7 +105,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
@@ -116,11 +116,17 @@ impl SlashCommand for FileSlashCommand {
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|path_match| {
|
||||
format!(
|
||||
let text = format!(
|
||||
"{}{}",
|
||||
path_match.path_prefix,
|
||||
path_match.path.to_string_lossy()
|
||||
)
|
||||
);
|
||||
|
||||
ArgumentCompletion {
|
||||
label: text.clone(),
|
||||
new_text: text,
|
||||
run_command: true,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
|
||||
@@ -2,7 +2,9 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use chrono::Local;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
@@ -34,7 +36,7 @@ impl SlashCommand for NowSlashCommand {
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
@@ -107,7 +107,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
@@ -33,13 +33,20 @@ impl SlashCommand for PromptSlashCommand {
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let prompts = store.await?.search(query).await;
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.filter_map(|prompt| Some(prompt.title?.to_string()))
|
||||
.filter_map(|prompt| {
|
||||
let prompt_title = prompt.title?.to_string();
|
||||
Some(ArgumentCompletion {
|
||||
label: prompt_title.clone(),
|
||||
new_text: prompt_title,
|
||||
run_command: true,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::{
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{CodeLabel, LineEnding, LspAdapterDelegate};
|
||||
use semantic_index::SemanticIndex;
|
||||
@@ -46,7 +46,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::{
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::ArgumentCompletion;
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Entity, Task, WeakView};
|
||||
@@ -37,7 +38,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
_cancel: Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
@@ -42,8 +44,12 @@ impl SlashCommand for TermSlashCommand {
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(vec![LINE_COUNT_ARG.to_string()]))
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(vec![ArgumentCompletion {
|
||||
label: LINE_COUNT_ARG.to_string(),
|
||||
new_text: LINE_COUNT_ARG.to_string(),
|
||||
run_command: true,
|
||||
}]))
|
||||
}
|
||||
|
||||
fn run(
|
||||
|
||||
@@ -1026,9 +1026,10 @@ impl Codegen {
|
||||
|
||||
let telemetry = self.telemetry.clone();
|
||||
let model_telemetry_id = prompt.model.telemetry_id();
|
||||
let response = CompletionProvider::global(cx).complete(prompt);
|
||||
let response = CompletionProvider::global(cx).complete(prompt, cx);
|
||||
|
||||
self.generation = cx.spawn(|this, mut cx| async move {
|
||||
let response = response.await;
|
||||
let generate = async {
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
|
||||
@@ -1036,7 +1037,7 @@ impl Codegen {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let task = async {
|
||||
let mut response = response.await?;
|
||||
let mut response = response.inner.await?;
|
||||
while let Some(chunk) = response.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
|
||||
@@ -15,6 +15,16 @@ pub fn init(cx: &mut AppContext) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArgumentCompletion {
|
||||
/// The label to display for this completion.
|
||||
pub label: String,
|
||||
/// The new text that should be inserted into the command when this completion is accepted.
|
||||
pub new_text: String,
|
||||
/// Whether the command should be run when accepting this completion.
|
||||
pub run_command: bool,
|
||||
}
|
||||
|
||||
pub trait SlashCommand: 'static + Send + Sync {
|
||||
fn name(&self) -> String;
|
||||
fn label(&self, _cx: &AppContext) -> CodeLabel {
|
||||
@@ -28,7 +38,7 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>>;
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
@@ -57,7 +67,7 @@ pub struct SlashCommandOutput {
|
||||
pub run_commands_in_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SlashCommandOutputSection<T> {
|
||||
pub range: Range<T>,
|
||||
pub icon: IconName,
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Render for Breadcrumbs {
|
||||
.into_any()
|
||||
});
|
||||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||
Label::new("›").color(Color::Muted).into_any_element()
|
||||
Label::new("›").color(Color::Placeholder).into_any_element()
|
||||
});
|
||||
|
||||
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
|
||||
@@ -83,7 +83,7 @@ impl Render for Breadcrumbs {
|
||||
Some(editor) => element.child(
|
||||
ButtonLike::new("toggle outline view")
|
||||
.child(breadcrumbs_stack)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(move |_, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
|
||||
|
||||
@@ -217,6 +217,9 @@ pub struct Client {
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
rpc_url: RwLock<Option<Url>>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -527,6 +530,8 @@ impl Client {
|
||||
authenticate: Default::default(),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
establish_connection: Default::default(),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
rpc_url: RwLock::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -584,6 +589,12 @@ impl Client {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn override_rpc_url(&self, url: Url) -> &Self {
|
||||
*self.rpc_url.write() = Some(url);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||
cx.global::<GlobalClient>().0.clone()
|
||||
}
|
||||
@@ -1086,38 +1097,50 @@ impl Client {
|
||||
self.establish_websocket_connection(credentials, cx)
|
||||
}
|
||||
|
||||
async fn get_rpc_url(
|
||||
fn rpc_url(
|
||||
&self,
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
release_channel: Option<ReleaseChannel>,
|
||||
) -> Result<Url> {
|
||||
if let Some(url) = &*ZED_RPC_URL {
|
||||
return Url::parse(url).context("invalid rpc url");
|
||||
}
|
||||
) -> impl Future<Output = Result<Url>> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let url_override = self.rpc_url.read().clone();
|
||||
|
||||
let mut url = http.build_url("/rpc");
|
||||
if let Some(preview_param) =
|
||||
release_channel.and_then(|channel| channel.release_query_param())
|
||||
{
|
||||
url += "?";
|
||||
url += preview_param;
|
||||
}
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
let collab_url = if response.status().is_redirection() {
|
||||
response
|
||||
.headers()
|
||||
.get("Location")
|
||||
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
||||
.to_str()
|
||||
.map_err(EstablishConnectionError::other)?
|
||||
.to_string()
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"unexpected /rpc response status {}",
|
||||
response.status()
|
||||
))?
|
||||
};
|
||||
async move {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some(url) = url_override {
|
||||
return Ok(url);
|
||||
}
|
||||
|
||||
Url::parse(&collab_url).context("invalid rpc url")
|
||||
if let Some(url) = &*ZED_RPC_URL {
|
||||
return Url::parse(url).context("invalid rpc url");
|
||||
}
|
||||
|
||||
let mut url = http.build_url("/rpc");
|
||||
if let Some(preview_param) =
|
||||
release_channel.and_then(|channel| channel.release_query_param())
|
||||
{
|
||||
url += "?";
|
||||
url += preview_param;
|
||||
}
|
||||
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
let collab_url = if response.status().is_redirection() {
|
||||
response
|
||||
.headers()
|
||||
.get("Location")
|
||||
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
||||
.to_str()
|
||||
.map_err(EstablishConnectionError::other)?
|
||||
.to_string()
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"unexpected /rpc response status {}",
|
||||
response.status()
|
||||
))?
|
||||
};
|
||||
|
||||
Url::parse(&collab_url).context("invalid rpc url")
|
||||
}
|
||||
}
|
||||
|
||||
fn establish_websocket_connection(
|
||||
@@ -1144,8 +1167,9 @@ impl Client {
|
||||
);
|
||||
|
||||
let http = self.http.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
|
||||
let mut rpc_url = rpc_url.await?;
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1186,6 +1210,7 @@ impl Client {
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<Credentials>> {
|
||||
let http = self.http.clone();
|
||||
let this = self.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let background = cx.background_executor().clone();
|
||||
|
||||
@@ -1215,7 +1240,8 @@ impl Client {
|
||||
{
|
||||
eprintln!("authenticate as admin {login}, {token}");
|
||||
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone())
|
||||
return this
|
||||
.authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1303,6 +1329,7 @@ impl Client {
|
||||
}
|
||||
|
||||
async fn authenticate_as_admin(
|
||||
self: &Arc<Self>,
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
login: String,
|
||||
mut api_token: String,
|
||||
@@ -1319,7 +1346,7 @@ impl Client {
|
||||
|
||||
// Use the collab server's admin API to retrieve the id
|
||||
// of the impersonated user.
|
||||
let mut url = Self::get_rpc_url(http.clone(), None).await?;
|
||||
let mut url = self.rpc_url(http.clone(), None).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request = Request::get(url.as_str())
|
||||
|
||||
@@ -227,7 +227,7 @@ impl Telemetry {
|
||||
let state = state.clone();
|
||||
async move {
|
||||
if let Some(tempfile) =
|
||||
NamedTempFile::new_in(paths::config_dir().as_path()).log_err()
|
||||
NamedTempFile::new_in(paths::logs_dir().as_path()).log_err()
|
||||
{
|
||||
state.lock().log_file = Some(tempfile);
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ test-support = ["dep:parking_lot"]
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
parking_lot = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod system_clock;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
@@ -16,7 +17,7 @@ pub type Seq = u32;
|
||||
|
||||
/// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
|
||||
/// used to determine the ordering of events in the editor.
|
||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Lamport {
|
||||
pub replica_id: ReplicaId,
|
||||
pub value: Seq,
|
||||
@@ -161,6 +162,10 @@ impl Lamport {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
((self.value as u64) << 32) | (self.replica_id as u64)
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) -> Self {
|
||||
let timestamp = *self;
|
||||
self.value += 1;
|
||||
|
||||
@@ -71,6 +71,7 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
async-trait.workspace = true
|
||||
audio.workspace = true
|
||||
call = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -562,7 +562,7 @@ fn test_fuzzy_like_string() {
|
||||
assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
|
||||
}
|
||||
|
||||
#[cfg(target = "macos")]
|
||||
#[cfg(target_os = "macos")]
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_search_users(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = tests::TestDb::postgres(cx.executor());
|
||||
|
||||
@@ -595,6 +595,14 @@ impl Server {
|
||||
.add_message_handler(user_message_handler(acknowledge_channel_message))
|
||||
.add_message_handler(user_message_handler(acknowledge_buffer_version))
|
||||
.add_request_handler(user_handler(get_supermaven_api_key))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::OpenContext>,
|
||||
))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::SynchronizeContexts>,
|
||||
))
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context)
|
||||
.add_streaming_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
move |request, response, session| {
|
||||
@@ -3056,6 +3064,53 @@ async fn update_buffer(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_context(message: proto::UpdateContext, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(message.project_id);
|
||||
|
||||
let operation = message.operation.as_ref().context("invalid operation")?;
|
||||
let capability = match operation.variant.as_ref() {
|
||||
Some(proto::context_operation::Variant::BufferOperation(buffer_op)) => {
|
||||
if let Some(buffer_op) = buffer_op.operation.as_ref() {
|
||||
match buffer_op.variant {
|
||||
None | Some(proto::operation::Variant::UpdateSelections(_)) => {
|
||||
Capability::ReadOnly
|
||||
}
|
||||
_ => Capability::ReadWrite,
|
||||
}
|
||||
} else {
|
||||
Capability::ReadWrite
|
||||
}
|
||||
}
|
||||
Some(_) => Capability::ReadWrite,
|
||||
None => Capability::ReadOnly,
|
||||
};
|
||||
|
||||
let guard = session
|
||||
.db()
|
||||
.await
|
||||
.connections_for_buffer_update(
|
||||
project_id,
|
||||
session.principal_id(),
|
||||
session.connection_id,
|
||||
capability,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (host, guests) = &*guard;
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
guests.iter().chain([host]).copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, message.clone())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that a project has been updated.
|
||||
async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
|
||||
request: T,
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant::ContextStore;
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{User, RECEIVE_TIMEOUT};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -6449,3 +6450,123 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
assert!(!pane.can_navigate_forward());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_context_collaboration_with_reconnect(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a.fs().insert_tree("/a", Default::default()).await;
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
// Client A sees that a guest has joined.
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
let context_store_a = cx_a
|
||||
.update(|cx| ContextStore::new(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let context_store_b = cx_b
|
||||
.update(|cx| ContextStore::new(project_b.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A creates a new context.
|
||||
let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
|
||||
executor.run_until_parked();
|
||||
|
||||
// Client B retrieves host's contexts and joins one.
|
||||
let context_b = context_store_b
|
||||
.update(cx_b, |store, cx| {
|
||||
let host_contexts = store.host_contexts().to_vec();
|
||||
assert_eq!(host_contexts.len(), 1);
|
||||
store.open_remote_context(host_contexts[0].id.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Host and guest make changes
|
||||
context_a.update(cx_a, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Host change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
context_b.update(cx_b, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Guest change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Disconnect client A and make some changes while disconnected.
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
server.forbid_connections();
|
||||
context_a.update(cx_a, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Host offline change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
context_b.update(cx_b, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Guest offline change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
|
||||
"Host offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Allow client A to reconnect and verify that contexts converge.
|
||||
server.allow_connections();
|
||||
executor.advance_clock(RECEIVE_TIMEOUT);
|
||||
assert_eq!(
|
||||
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Client A disconnects without being able to reconnect. Context B becomes readonly.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
context_b.read_with(cx_b, |context, cx| {
|
||||
assert!(context.buffer().read(cx).read_only());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,6 +294,8 @@ impl TestServer {
|
||||
menu::init();
|
||||
dev_server_projects::init(client.clone(), cx);
|
||||
settings::KeymapFile::load_asset(os_keymap, cx).unwrap();
|
||||
assistant::FakeCompletionProvider::setup_test(cx);
|
||||
assistant::context_store::init(&client);
|
||||
});
|
||||
|
||||
client
|
||||
|
||||
@@ -355,11 +355,10 @@ impl ChatPanel {
|
||||
.child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
|
||||
.child(Avatar::new(user_being_replied_to.avatar_uri.clone()).size(rems(0.7)))
|
||||
.child(
|
||||
div().font_weight(FontWeight::SEMIBOLD).child(
|
||||
Label::new(format!("@{}", user_being_replied_to.github_login))
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
Label::new(format!("@{}", user_being_replied_to.github_login))
|
||||
.size(LabelSize::XSmall)
|
||||
.weight(FontWeight::SEMIBOLD)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
div().overflow_y_hidden().child(
|
||||
@@ -490,22 +489,16 @@ impl ChatPanel {
|
||||
|this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_ui_sm(cx)
|
||||
.child(
|
||||
div().absolute().child(
|
||||
Avatar::new(message.sender.avatar_uri.clone())
|
||||
.size(rems(1.)),
|
||||
),
|
||||
Avatar::new(message.sender.avatar_uri.clone())
|
||||
.size(rems(1.)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pl(cx.rem_size() + px(6.0))
|
||||
.pr(px(8.0))
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.child(
|
||||
Label::new(message.sender.github_login.clone())
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
Label::new(message.sender.github_login.clone())
|
||||
.size(LabelSize::Small)
|
||||
.weight(FontWeight::BOLD),
|
||||
)
|
||||
.child(
|
||||
Label::new(time_format::format_localized_timestamp(
|
||||
@@ -1044,13 +1037,12 @@ impl Render for ChatPanel {
|
||||
.id(("reply-preview", reply_to_message_id))
|
||||
.child(Label::new("Replying to ").size(LabelSize::Small))
|
||||
.child(
|
||||
div().font_weight(FontWeight::BOLD).child(
|
||||
Label::new(format!(
|
||||
"@{}",
|
||||
user_being_replied_to.github_login.clone()
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
Label::new(format!(
|
||||
"@{}",
|
||||
user_being_replied_to.github_login.clone()
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.weight(FontWeight::BOLD),
|
||||
)
|
||||
.when_some(channel_id, |this, channel_id| {
|
||||
this.cursor_pointer().on_click(cx.listener(
|
||||
|
||||
@@ -2547,9 +2547,8 @@ impl CollabPanel {
|
||||
.take(FACEPILE_LIMIT)
|
||||
.chain(if extra_count > 0 {
|
||||
Some(
|
||||
div()
|
||||
Label::new(format!("+{extra_count}"))
|
||||
.ml_2()
|
||||
.child(Label::new(format!("+{extra_count}")))
|
||||
.into_any_element(),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -49,6 +49,7 @@ lazy_static.workspace = true
|
||||
linkify.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
@@ -258,7 +258,7 @@ gpui::actions!(
|
||||
RedoSelection,
|
||||
Rename,
|
||||
RestartLanguageServer,
|
||||
RevealInFinder,
|
||||
RevealInFileManager,
|
||||
ReverseLines,
|
||||
RevertSelectedHunks,
|
||||
ScrollCursorBottom,
|
||||
|
||||
@@ -89,13 +89,16 @@ use language::{
|
||||
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||
Point, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
use language::{BufferRow, Runnable, RunnableRange};
|
||||
use language::{point_to_lsp, BufferRow, Runnable, RunnableRange};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||
|
||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
||||
pub use lsp::CompletionContext;
|
||||
use lsp::{CompletionTriggerKind, DiagnosticSeverity, LanguageServerId};
|
||||
use lsp::{
|
||||
CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat,
|
||||
LanguageServerId,
|
||||
};
|
||||
use mouse_context_menu::MouseContextMenu;
|
||||
use movement::TextLayoutDetails;
|
||||
pub use multi_buffer::{
|
||||
@@ -1126,11 +1129,10 @@ impl CompletionsMenu {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
h_flex().ml_4().child(
|
||||
Label::new(text.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
Label::new(text.clone())
|
||||
.ml_4()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -1153,7 +1155,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
}))
|
||||
.child(h_flex().overflow_hidden().child(completion_label))
|
||||
.end_slot::<Div>(documentation_label),
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
@@ -1927,6 +1929,11 @@ impl Editor {
|
||||
EditorMode::AutoHeight { .. } => "auto_height",
|
||||
EditorMode::Full => "full",
|
||||
};
|
||||
|
||||
if EditorSettings::get_global(cx).jupyter.enabled {
|
||||
key_context.add("jupyter");
|
||||
}
|
||||
|
||||
key_context.set("mode", mode);
|
||||
if self.pending_rename.is_some() {
|
||||
key_context.add("renaming");
|
||||
@@ -2352,7 +2359,9 @@ impl Editor {
|
||||
drop(context_menu);
|
||||
}
|
||||
|
||||
hide_hover(self, cx);
|
||||
if !self.hover_state.focused(cx) {
|
||||
hide_hover(self, cx);
|
||||
}
|
||||
|
||||
if old_cursor_position.to_display_point(&display_map).row()
|
||||
!= new_cursor_position.to_display_point(&display_map).row()
|
||||
@@ -2855,8 +2864,10 @@ impl Editor {
|
||||
return true;
|
||||
}
|
||||
|
||||
if hide_hover(self, cx) {
|
||||
return true;
|
||||
if !self.hover_state.focused(cx) {
|
||||
if hide_hover(self, cx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.hide_context_menu(cx).is_some() {
|
||||
@@ -5152,7 +5163,6 @@ impl Editor {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
if let Some(tabstop) = tabstops.first() {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(tabstop.ranges.iter().cloned());
|
||||
@@ -10326,7 +10336,7 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
cx.reveal_path(&file.abs_path(cx));
|
||||
@@ -11087,6 +11097,7 @@ impl Editor {
|
||||
if *singleton_buffer_edited {
|
||||
if let Some(project) = &self.project {
|
||||
let project = project.read(cx);
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let languages_affected = multibuffer
|
||||
.read(cx)
|
||||
.all_buffers()
|
||||
@@ -11597,8 +11608,11 @@ impl Editor {
|
||||
if let Some(blame) = self.blame.as_ref() {
|
||||
blame.update(cx, GitBlame::blur)
|
||||
}
|
||||
if !self.hover_state.focused(cx) {
|
||||
hide_hover(self, cx);
|
||||
}
|
||||
|
||||
self.hide_context_menu(cx);
|
||||
hide_hover(self, cx);
|
||||
cx.emit(EditorEvent::Blurred);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -11752,6 +11766,97 @@ pub trait CompletionProvider {
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
fn snippet_completions(
|
||||
project: &Project,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: text::Anchor,
|
||||
cx: &mut AppContext,
|
||||
) -> Vec<Completion> {
|
||||
let language = buffer.read(cx).language_at(buffer_position);
|
||||
let language_name = language.as_ref().map(|language| language.lsp_id());
|
||||
let snippet_store = project.snippets().read(cx);
|
||||
let snippets = snippet_store.snippets_for(language_name, cx);
|
||||
|
||||
if snippets.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let snapshot = buffer.read(cx).text_snapshot();
|
||||
let chunks = snapshot.reversed_chunks_in_range(text::Anchor::MIN..buffer_position);
|
||||
|
||||
let mut lines = chunks.lines();
|
||||
let Some(line_at) = lines.next().filter(|line| !line.is_empty()) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let scope = language.map(|language| language.default_scope());
|
||||
let mut last_word = line_at
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| char_kind(&scope, *c) == CharKind::Word)
|
||||
.collect::<String>();
|
||||
last_word = last_word.chars().rev().collect();
|
||||
let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot);
|
||||
let to_lsp = |point: &text::Anchor| {
|
||||
let end = text::ToPointUtf16::to_point_utf16(point, &snapshot);
|
||||
point_to_lsp(end)
|
||||
};
|
||||
let lsp_end = to_lsp(&buffer_position);
|
||||
snippets
|
||||
.into_iter()
|
||||
.filter_map(|snippet| {
|
||||
let matching_prefix = snippet
|
||||
.prefix
|
||||
.iter()
|
||||
.find(|prefix| prefix.starts_with(&last_word))?;
|
||||
let start = as_offset - last_word.len();
|
||||
let start = snapshot.anchor_before(start);
|
||||
let range = start..buffer_position;
|
||||
let lsp_start = to_lsp(&start);
|
||||
let lsp_range = lsp::Range {
|
||||
start: lsp_start,
|
||||
end: lsp_end,
|
||||
};
|
||||
Some(Completion {
|
||||
old_range: range,
|
||||
new_text: snippet.body.clone(),
|
||||
label: CodeLabel {
|
||||
text: matching_prefix.clone(),
|
||||
runs: vec![],
|
||||
filter_range: 0..matching_prefix.len(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: snippet
|
||||
.description
|
||||
.clone()
|
||||
.map(|description| Documentation::SingleLine(description)),
|
||||
lsp_completion: lsp::CompletionItem {
|
||||
label: snippet.prefix.first().unwrap().clone(),
|
||||
kind: Some(CompletionItemKind::SNIPPET),
|
||||
label_details: snippet.description.as_ref().map(|description| {
|
||||
lsp::CompletionItemLabelDetails {
|
||||
detail: Some(description.clone()),
|
||||
description: None,
|
||||
}
|
||||
}),
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: snippet.body.clone(),
|
||||
insert: lsp_range,
|
||||
replace: lsp_range,
|
||||
},
|
||||
)),
|
||||
filter_text: Some(snippet.body.clone()),
|
||||
sort_text: Some(char::MAX.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
confirm: None,
|
||||
show_new_completions_on_confirm: false,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl CompletionProvider for Model<Project> {
|
||||
fn completions(
|
||||
&self,
|
||||
@@ -11761,7 +11866,14 @@ impl CompletionProvider for Model<Project> {
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.completions(&buffer, buffer_position, options, cx)
|
||||
let snippets = snippet_completions(project, buffer, buffer_position, cx);
|
||||
let project_completions = project.completions(&buffer, buffer_position, options, cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut completions = project_completions.await?;
|
||||
//let snippets = snippets.into_iter().;
|
||||
completions.extend(snippets);
|
||||
Ok(completions)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12719,7 +12831,7 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> +
|
||||
})
|
||||
}
|
||||
|
||||
trait RangeToAnchorExt {
|
||||
pub trait RangeToAnchorExt {
|
||||
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct EditorSettings {
|
||||
pub expand_excerpt_lines: u32,
|
||||
#[serde(default)]
|
||||
pub double_click_in_multibuffer: DoubleClickInMultibuffer,
|
||||
#[serde(default)]
|
||||
pub jupyter: Jupyter,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -64,6 +66,15 @@ pub enum DoubleClickInMultibuffer {
|
||||
Open,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Jupyter {
|
||||
/// Whether the Jupyter feature is enabled.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Toolbar {
|
||||
pub breadcrumbs: bool,
|
||||
@@ -217,6 +228,9 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: select
|
||||
pub double_click_in_multibuffer: Option<DoubleClickInMultibuffer>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
pub jupyter: Option<Jupyter>,
|
||||
}
|
||||
|
||||
// Toolbar related settings
|
||||
|
||||
@@ -709,18 +709,24 @@ impl EditorElement {
|
||||
let Some(hub) = editor.collaboration_hub() else {
|
||||
return;
|
||||
};
|
||||
let range = DisplayPoint::new(point.row(), point.column().saturating_sub(1))
|
||||
..DisplayPoint::new(
|
||||
let start = snapshot.display_snapshot.clip_point(
|
||||
DisplayPoint::new(point.row(), point.column().saturating_sub(1)),
|
||||
Bias::Left,
|
||||
);
|
||||
let end = snapshot.display_snapshot.clip_point(
|
||||
DisplayPoint::new(
|
||||
point.row(),
|
||||
(point.column() + 1).min(snapshot.line_len(point.row())),
|
||||
);
|
||||
),
|
||||
Bias::Right,
|
||||
);
|
||||
|
||||
let range = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_at(range.start.to_point(&snapshot.display_snapshot), Bias::Left)
|
||||
.anchor_at(start.to_point(&snapshot.display_snapshot), Bias::Left)
|
||||
..snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right);
|
||||
.anchor_at(end.to_point(&snapshot.display_snapshot), Bias::Right);
|
||||
|
||||
let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
|
||||
return;
|
||||
@@ -3660,6 +3666,11 @@ impl EditorElement {
|
||||
if scroll_position != current_scroll_position {
|
||||
editor.scroll(scroll_position, axis, cx);
|
||||
cx.stop_propagation();
|
||||
} else if y < 0. {
|
||||
// Due to clamping, we may fail to detect cases of overscroll to the top;
|
||||
// We want the scroll manager to get an update in such cases and detect the change of direction
|
||||
// on the next frame.
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -423,7 +423,9 @@ pub fn update_inlay_link_and_hover_points(
|
||||
editor.hide_hovered_link(cx)
|
||||
}
|
||||
if !hover_updated {
|
||||
hover_popover::hover_at(editor, None, cx);
|
||||
if !editor.hover_state.focused(cx) {
|
||||
hover_popover::hover_at(editor, None, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,24 +5,24 @@ use crate::{
|
||||
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
|
||||
EditorStyle, Hover, RangeToAnchorExt,
|
||||
};
|
||||
use futures::{stream::FuturesUnordered, FutureExt};
|
||||
use gpui::{
|
||||
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
|
||||
Task, ViewContext, WeakView,
|
||||
div, px, AnyElement, AsyncWindowContext, CursorStyle, Hsla, InteractiveElement, IntoElement,
|
||||
Length, MouseButton, ParentElement, Pixels, ScrollHandle, SharedString, Size,
|
||||
StatefulInteractiveElement, Styled, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
|
||||
|
||||
use language::{DiagnosticEntry, Language, LanguageRegistry};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use multi_buffer::ToOffset;
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
|
||||
use project::{HoverBlock, InlayHintLabelPart};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, window_is_transparent, Tooltip};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
@@ -40,14 +40,31 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
|
||||
/// depending on whether a point to hover over is provided.
|
||||
pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) {
|
||||
if EditorSettings::get_global(cx).hover_popover_enabled {
|
||||
if let Some(anchor) = anchor {
|
||||
show_hover(editor, anchor, false, cx);
|
||||
} else {
|
||||
hide_hover(editor, cx);
|
||||
let old_hover_shown = show_old_hover(editor, cx);
|
||||
if !old_hover_shown {
|
||||
if let Some(anchor) = anchor {
|
||||
show_hover(editor, anchor, false, cx);
|
||||
} else {
|
||||
hide_hover(editor, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_old_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let info_popovers = editor.hover_state.info_popovers.clone();
|
||||
for p in info_popovers {
|
||||
let keyboard_grace = p.keyboard_grace.borrow();
|
||||
if *keyboard_grace {
|
||||
if let Some(anchor) = p.anchor {
|
||||
show_hover(editor, anchor, false, cx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub struct InlayHover {
|
||||
pub range: InlayHighlight,
|
||||
pub tooltip: HoverBlock,
|
||||
@@ -113,12 +130,14 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
|
||||
let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
|
||||
let blocks = vec![inlay_hover.tooltip];
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
|
||||
|
||||
let hover_popover = InfoPopover {
|
||||
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
keyboard_grace: Rc::new(RefCell::new(false)),
|
||||
anchor: None,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -291,39 +310,40 @@ fn show_hover(
|
||||
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
|
||||
let mut hover_highlights = Vec::with_capacity(hovers_response.len());
|
||||
let mut info_popovers = Vec::with_capacity(hovers_response.len());
|
||||
let mut info_popover_tasks = hovers_response
|
||||
.into_iter()
|
||||
.map(|hover_result| async {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = hover_result
|
||||
.range
|
||||
.and_then(|range| {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
|
||||
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| anchor..anchor);
|
||||
for hover_result in hovers_response {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = hover_result
|
||||
.range
|
||||
.and_then(|range| {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
|
||||
let blocks = hover_result.contents;
|
||||
let language = hover_result.language;
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| anchor..anchor);
|
||||
|
||||
(
|
||||
range.clone(),
|
||||
InfoPopover {
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
while let Some((highlight_range, info_popover)) = info_popover_tasks.next().await {
|
||||
let blocks = hover_result.contents;
|
||||
let language = hover_result.language;
|
||||
let parsed_content =
|
||||
parse_blocks(&blocks, &language_registry, language, &mut cx).await;
|
||||
info_popover_tasks.push((
|
||||
range.clone(),
|
||||
InfoPopover {
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
|
||||
anchor: Some(anchor),
|
||||
},
|
||||
));
|
||||
}
|
||||
for (highlight_range, info_popover) in info_popover_tasks {
|
||||
hover_highlights.push(highlight_range);
|
||||
info_popovers.push(info_popover);
|
||||
}
|
||||
@@ -353,76 +373,122 @@ fn show_hover(
|
||||
editor.hover_state.info_task = Some(task);
|
||||
}
|
||||
|
||||
fn transform_codeblock(mut input: String) -> String {
|
||||
input = input.replace("\\n", "\n").trim().to_string();
|
||||
|
||||
let lines: Vec<&str> = input.lines().collect();
|
||||
let mut result: Vec<String> = Vec::new();
|
||||
let mut i = 0;
|
||||
let mut open_block = false;
|
||||
while i < lines.len() {
|
||||
let mut line = lines[i].to_string();
|
||||
if line.starts_with("```") {
|
||||
open_block = !open_block;
|
||||
} else {
|
||||
//remove mid-sentence single linebreaks
|
||||
while i + 1 < lines.len()
|
||||
&& !lines[i + 1].trim().is_empty()
|
||||
&& !open_block
|
||||
&& !lines[i + 1].contains("```")
|
||||
{
|
||||
i += 1;
|
||||
if line.contains('\n') {
|
||||
break;
|
||||
}
|
||||
line.push(' ');
|
||||
line.push_str(lines[i]);
|
||||
}
|
||||
}
|
||||
result.push(line);
|
||||
i += 1;
|
||||
}
|
||||
let r = result.join("\n");
|
||||
r
|
||||
}
|
||||
|
||||
async fn parse_blocks(
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
) -> markdown::ParsedMarkdown {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Option<View<Markdown>> {
|
||||
let mut combined_text = String::new();
|
||||
let fallback_language_name = if let Some(ref l) = language {
|
||||
let l = Arc::clone(l);
|
||||
Some(l.lsp_id().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for block in blocks {
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
markdown::new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text.replace("\\n", "\n"));
|
||||
}
|
||||
let text = transform_codeblock(block.clone().text);
|
||||
|
||||
HoverBlockKind::Markdown => {
|
||||
markdown::parse_markdown_block(
|
||||
&block.text.replace("\\n", "\n"),
|
||||
language_registry,
|
||||
language.clone(),
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut region_ranges,
|
||||
&mut regions,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
HoverBlockKind::Code { language } => {
|
||||
if let Some(language) = language_registry
|
||||
.language_for_name(language)
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
{
|
||||
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
|
||||
} else {
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
combined_text.push_str(text.as_str());
|
||||
}
|
||||
let rendered_block = cx
|
||||
.new_view(|cx| {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let buffer_font_family = settings.buffer_font.family.clone();
|
||||
let mut base_style = cx.text_style();
|
||||
base_style.refine(&gpui::TextStyleRefinement {
|
||||
font_family: Some(buffer_font_family.clone()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let leading_space = text.chars().take_while(|c| c.is_whitespace()).count();
|
||||
if leading_space > 0 {
|
||||
highlights = highlights
|
||||
.into_iter()
|
||||
.map(|(range, style)| {
|
||||
(
|
||||
range.start.saturating_sub(leading_space)
|
||||
..range.end.saturating_sub(leading_space),
|
||||
style,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
region_ranges = region_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: base_style,
|
||||
code_block: gpui::StyleRefinement {
|
||||
text: Some(gpui::TextStyleRefinement {
|
||||
font_family: Some(buffer_font_family.clone()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
..Default::default()
|
||||
}),
|
||||
margin: gpui::EdgesRefinement {
|
||||
top: Some(Length::Definite(rems(1.).into())),
|
||||
left: None,
|
||||
right: None,
|
||||
bottom: Some(Length::Definite(rems(1.).into())),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: gpui::TextStyleRefinement {
|
||||
font_family: Some(buffer_font_family.clone()),
|
||||
background_color: Some(cx.theme().colors().background),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Color::Muted.color(cx),
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: gpui::TextStyleRefinement {
|
||||
font_family: Some(buffer_font_family.clone()),
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: gpui::TextStyleRefinement {
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
break_style: Default::default(),
|
||||
};
|
||||
|
||||
ParsedMarkdown {
|
||||
text: text.trim().to_string(),
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
Markdown::new(
|
||||
combined_text,
|
||||
markdown_style.clone(),
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
fallback_language_name,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
|
||||
rendered_block
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -444,7 +510,7 @@ impl HoverState {
|
||||
style: &EditorStyle,
|
||||
visible_rows: Range<DisplayRow>,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<AnyElement>)> {
|
||||
// If there is a diagnostic, position the popovers based on that.
|
||||
@@ -482,29 +548,39 @@ impl HoverState {
|
||||
elements.push(diagnostic_popover.render(style, max_size, cx));
|
||||
}
|
||||
for info_popover in &mut self.info_popovers {
|
||||
elements.push(info_popover.render(style, max_size, workspace.clone(), cx));
|
||||
elements.push(info_popover.render(max_size, cx));
|
||||
}
|
||||
|
||||
Some((point, elements))
|
||||
}
|
||||
|
||||
pub fn focused(&self, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let mut hover_popover_is_focused = false;
|
||||
for info_popover in &self.info_popovers {
|
||||
for markdown_view in &info_popover.parsed_content {
|
||||
if markdown_view.focus_handle(cx).is_focused(cx) {
|
||||
hover_popover_is_focused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hover_popover_is_focused;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
pub struct InfoPopover {
|
||||
pub symbol_range: RangeInEditor,
|
||||
pub parsed_content: ParsedMarkdown,
|
||||
pub parsed_content: Option<View<Markdown>>,
|
||||
pub scroll_handle: ScrollHandle,
|
||||
pub keyboard_grace: Rc<RefCell<bool>>,
|
||||
pub anchor: Option<Anchor>,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
pub fn render(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
|
||||
let keyboard_grace = Rc::clone(&self.keyboard_grace);
|
||||
let mut d = div()
|
||||
.id("info_popover")
|
||||
.elevation_2(cx)
|
||||
.overflow_y_scroll()
|
||||
@@ -514,15 +590,17 @@ impl InfoPopover {
|
||||
// Prevent a mouse down/move on the popover from being propagated to the editor,
|
||||
// because that would dismiss the popover.
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(div().p_2().child(crate::render_parsed_markdown(
|
||||
"content",
|
||||
&self.parsed_content,
|
||||
style,
|
||||
workspace,
|
||||
cx,
|
||||
)))
|
||||
.into_any_element()
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
let mut keyboard_grace = keyboard_grace.borrow_mut();
|
||||
*keyboard_grace = false;
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.p_2();
|
||||
|
||||
if let Some(markdown) = &self.parsed_content {
|
||||
d = d.child(markdown.clone());
|
||||
}
|
||||
d.into_any_element()
|
||||
}
|
||||
|
||||
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
|
||||
@@ -587,15 +665,12 @@ impl DiagnosticPopover {
|
||||
div()
|
||||
.id("diagnostic")
|
||||
.block()
|
||||
.elevation_2(cx)
|
||||
.overflow_y_scroll()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(diagnostic_colors.background)
|
||||
.text_color(style.text.color)
|
||||
.border_1()
|
||||
.border_color(diagnostic_colors.border)
|
||||
.rounded_md()
|
||||
.elevation_2_borderless(cx)
|
||||
// Don't draw the background color if the theme
|
||||
// allows transparent surfaces.
|
||||
.when(window_is_transparent(cx), |this| {
|
||||
this.bg(gpui::transparent_black())
|
||||
})
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
@@ -607,7 +682,19 @@ impl DiagnosticPopover {
|
||||
// because that would move the cursor.
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
|
||||
.child(SharedString::from(text))
|
||||
.child(
|
||||
div()
|
||||
.id("diagnostic-inner")
|
||||
.overflow_y_scroll()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(diagnostic_colors.background)
|
||||
.text_color(style.text.color)
|
||||
.border_1()
|
||||
.border_color(diagnostic_colors.border)
|
||||
.rounded_lg()
|
||||
.child(SharedString::from(text)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -633,17 +720,33 @@ mod tests {
|
||||
InlayId, PointForPosition,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use markdown::parser::MarkdownEvent;
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::atomic;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use text::Bias;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
impl InfoPopover {
|
||||
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
|
||||
let mut rendered_text = String::new();
|
||||
if let Some(parsed_content) = self.parsed_content.clone() {
|
||||
let markdown = parsed_content.read(cx);
|
||||
let text = markdown.parsed_markdown().source().to_string();
|
||||
let data = markdown.parsed_markdown().events();
|
||||
let slice = data;
|
||||
|
||||
for (range, event) in slice.iter() {
|
||||
if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
|
||||
rendered_text.push_str(&text[range.clone()])
|
||||
}
|
||||
}
|
||||
}
|
||||
rendered_text
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mouse_hover_info_popover_with_autocomplete_popover(
|
||||
@@ -727,7 +830,7 @@ mod tests {
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
requests.next().await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -735,14 +838,13 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// check that the completion menu is still visible and that there still has only been 1 completion request
|
||||
@@ -768,7 +870,7 @@ mod tests {
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
|
||||
//verify the information popover is still visible and unchanged
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -776,14 +878,14 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// Mouse moved with no hover response dismisses
|
||||
@@ -861,7 +963,7 @@ mod tests {
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
requests.next().await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -869,14 +971,14 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// Mouse moved with no hover response dismisses
|
||||
@@ -922,34 +1024,49 @@ mod tests {
|
||||
let symbol_range = cx.lsp_range(indoc! {"
|
||||
«fn» test() { println!(); }
|
||||
"});
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: "some other basic docs".to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _cx| {
|
||||
assert!(!editor.hover_state.visible());
|
||||
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
0,
|
||||
"Expected no hovers but got but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
});
|
||||
|
||||
let mut requests =
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: "some other basic docs".to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
});
|
||||
|
||||
requests.next().await;
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some other basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some other basic docs".to_string())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -989,30 +1106,38 @@ mod tests {
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
rendered_text,
|
||||
"regular text for hover to show".to_string(),
|
||||
"No empty string hovers should be shown"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_transform_info_popover_markdown(_cx: &mut gpui::TestAppContext) {
|
||||
//test removing middle of the block line-breaks
|
||||
let input =String::from( "```rust\nfn\n```\n\n---\n\nA function or function pointer.\n\nFunctions are the primary way code is executed within Rust. Function blocks, usually just\ncalled functions, can be defined in a variety of different places and be assigned many\ndifferent attributes and modifiers.\n\nStandalone functions that just sit within a module not attached to anything else are common,\nbut most functions will end up being inside [`impl`](https://doc.rust-lang.org/stable/std/keyword.impl.html) blocks, either on another type itself, or\nas a trait impl for that type.\n\n```rust\nfn standalone_function() {\n // code\n}\n\npub fn public_thing(argument: bool) -> String {\n // code\n}\n\nstruct Thing {\n foo: i32,\n}\n\nimpl Thing {\n pub fn new() -> Self {\n Self {\n foo: 42,\n }\n }\n}\n```\n\nIn addition to presenting fixed types in the form of `fn name(arg: type, ..) -> return_type`,\nfunctions can also declare a list of type parameters along with trait bounds that they fall\ninto.\n\n```rust\nfn generic_function<T: Clone>(x: T) -> (T, T, T) {\n (x.clone(), x.clone(), x.clone())\n}\n\nfn generic_where<T>(x: T) -> T\n where T: std::ops::Add<Output = T> + Copy\n{\n x + x + x\n}\n```\n\nDeclaring trait bounds in the angle brackets is functionally identical to using a `where`\nclause. It's up to the programmer to decide which works better in each situation, but `where`\ntends to be better when things get longer than one line.\n\nAlong with being made public via `pub`, `fn` can also have an [`extern`](https://doc.rust-lang.org/stable/std/keyword.extern.html) added for use in\nFFI.\n\nFor more information on the various types of functions and how they're used, consult the [Rust\nbook](https://doc.rust-lang.org/stable/book/ch03-03-how-functions-work.html) or the [Reference](https://doc.rust-lang.org/stable/reference/items/functions.html).");
|
||||
let target_output = String::from("```rust\nfn\n```\n ---\n A function or function pointer.\n Functions are the primary way code is executed within Rust. Function blocks, usually just called functions, can be defined in a variety of different places and be assigned many different attributes and modifiers.\n Standalone functions that just sit within a module not attached to anything else are common, but most functions will end up being inside [`impl`](https://doc.rust-lang.org/stable/std/keyword.impl.html) blocks, either on another type itself, or as a trait impl for that type.\n\n```rust\nfn standalone_function() {\n // code\n}\n\npub fn public_thing(argument: bool) -> String {\n // code\n}\n\nstruct Thing {\n foo: i32,\n}\n\nimpl Thing {\n pub fn new() -> Self {\n Self {\n foo: 42,\n }\n }\n}\n```\n In addition to presenting fixed types in the form of `fn name(arg: type, ..) -> return_type`, functions can also declare a list of type parameters along with trait bounds that they fall into.\n\n```rust\nfn generic_function<T: Clone>(x: T) -> (T, T, T) {\n (x.clone(), x.clone(), x.clone())\n}\n\nfn generic_where<T>(x: T) -> T\n where T: std::ops::Add<Output = T> + Copy\n{\n x + x + x\n}\n```\n Declaring trait bounds in the angle brackets is functionally identical to using a `where` clause. It's up to the programmer to decide which works better in each situation, but `where` tends to be better when things get longer than one line.\n Along with being made public via `pub`, `fn` can also have an [`extern`](https://doc.rust-lang.org/stable/std/keyword.extern.html) added for use in FFI.\n For more information on the various types of functions and how they're used, consult the [Rust book](https://doc.rust-lang.org/stable/book/ch03-03-how-functions-work.html) or the [Reference](https://doc.rust-lang.org/stable/reference/items/functions.html).");
|
||||
let output = transform_codeblock(input);
|
||||
assert_eq!(output, target_output);
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -1054,24 +1179,25 @@ mod tests {
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
code_str.trim(),
|
||||
rendered_text, code_str,
|
||||
"Should not have extra line breaks at end of rendered hover"
|
||||
);
|
||||
});
|
||||
@@ -1147,153 +1273,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let editor = cx.add_window(|cx| Editor::single_line(cx));
|
||||
editor
|
||||
.update(cx, |editor, _cx| {
|
||||
let style = editor.style.clone().unwrap();
|
||||
|
||||
struct Row {
|
||||
blocks: Vec<HoverBlock>,
|
||||
expected_marked_text: String,
|
||||
expected_styles: Vec<HighlightStyle>,
|
||||
}
|
||||
|
||||
let rows = &[
|
||||
// Strong emphasis
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "one **two** three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Links
|
||||
Row {
|
||||
blocks: vec three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Lists
|
||||
Row {
|
||||
blocks: vec
|
||||
- d"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
lists:
|
||||
- one
|
||||
- a
|
||||
- b
|
||||
- two
|
||||
- «c»
|
||||
- d"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Multi-paragraph list items
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "
|
||||
* one two
|
||||
three
|
||||
|
||||
* four five
|
||||
* six seven
|
||||
eight
|
||||
|
||||
nine
|
||||
* ten
|
||||
* six"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
- one two three
|
||||
- four five
|
||||
- six seven eight
|
||||
|
||||
nine
|
||||
- ten
|
||||
- six"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
for Row {
|
||||
blocks,
|
||||
expected_marked_text,
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
.into_iter()
|
||||
.zip(expected_styles.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
rendered.text, expected_text,
|
||||
"wrong text for input {blocks:?}"
|
||||
);
|
||||
|
||||
let rendered_highlights: Vec<_> = rendered
|
||||
.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight)| {
|
||||
let highlight = highlight.to_highlight_style(&style.syntax)?;
|
||||
Some((range.clone(), highlight))
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
rendered_highlights, expected_highlights,
|
||||
"wrong highlights for input {blocks:?}"
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
@@ -1537,9 +1516,8 @@ mod tests {
|
||||
"Popover range should match the new type label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover.parsed_content.text,
|
||||
format!("A tooltip for `{new_type_label}`"),
|
||||
"Rendered text should not anyhow alter backticks"
|
||||
popover.get_rendered_text(cx),
|
||||
format!("A tooltip for {new_type_label}"),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1593,7 +1571,7 @@ mod tests {
|
||||
"Popover range should match the struct label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover.parsed_content.text,
|
||||
popover.get_rendered_text(cx),
|
||||
format!("A tooltip for {struct_label}"),
|
||||
"Rendered markdown element should remove backticks from text"
|
||||
);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::{
|
||||
Copy, CopyPermalinkToLine, Cut, DisplayPoint, Editor, EditorMode, FindAllReferences,
|
||||
GoToDefinition, GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFinder,
|
||||
SelectMode, ToggleCodeActions,
|
||||
selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut, DisplayPoint,
|
||||
DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
|
||||
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, ToDisplayPoint,
|
||||
ToggleCodeActions,
|
||||
};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||
use workspace::OpenInTerminal;
|
||||
|
||||
@@ -37,6 +41,23 @@ impl MouseContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
fn display_ranges<'a>(
|
||||
display_map: &'a DisplaySnapshot,
|
||||
selections: &'a SelectionsCollection,
|
||||
) -> impl Iterator<Item = Range<DisplayPoint>> + 'a {
|
||||
let pending = selections
|
||||
.pending
|
||||
.as_ref()
|
||||
.map(|pending| &pending.selection);
|
||||
selections.disjoint.iter().chain(pending).map(move |s| {
|
||||
if s.reversed {
|
||||
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
|
||||
} else {
|
||||
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deploy_context_menu(
|
||||
editor: &mut Editor,
|
||||
position: Point<Pixels>,
|
||||
@@ -65,11 +86,14 @@ pub fn deploy_context_menu(
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
let display_map = editor.selections.display_map(cx);
|
||||
if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) {
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
}
|
||||
|
||||
let focus = cx.focused();
|
||||
ui::ContextMenu::build(cx, |menu, _cx| {
|
||||
@@ -90,7 +114,12 @@ pub fn deploy_context_menu(
|
||||
.action("Copy", Box::new(Copy))
|
||||
.action("Paste", Box::new(Paste))
|
||||
.separator()
|
||||
.action("Reveal in Finder", Box::new(RevealInFinder))
|
||||
.when(cfg!(target_os = "macos"), |builder| {
|
||||
builder.action("Reveal in Finder", Box::new(RevealInFileManager))
|
||||
})
|
||||
.when(cfg!(not(target_os = "macos")), |builder| {
|
||||
builder.action("Reveal in File Manager", Box::new(RevealInFileManager))
|
||||
})
|
||||
.action("Open in Terminal", Box::new(OpenInTerminal))
|
||||
.action("Copy Permalink", Box::new(CopyPermalinkToLine));
|
||||
match focus {
|
||||
|
||||
@@ -42,6 +42,7 @@ semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
theme.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
@@ -526,6 +526,11 @@ fn populate_defaults(manifest: &mut ExtensionManifest, extension_path: &Path) ->
|
||||
}
|
||||
}
|
||||
|
||||
let snippets_json_path = extension_path.join("snippets.json");
|
||||
if snippets_json_path.exists() {
|
||||
manifest.snippets = Some(snippets_json_path);
|
||||
}
|
||||
|
||||
// For legacy extensions on the v0 schema (aka, using `extension.json`), we want to populate the grammars in
|
||||
// the manifest using the contents of the `grammars` directory.
|
||||
if manifest.schema_version.is_v0() {
|
||||
|
||||
@@ -78,6 +78,8 @@ pub struct ExtensionManifest {
|
||||
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
|
||||
#[serde(default)]
|
||||
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
|
||||
#[serde(default)]
|
||||
pub snippets: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
@@ -206,5 +208,6 @@ fn manifest_from_old_manifest(
|
||||
language_servers: Default::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use language::LspAdapterDelegate;
|
||||
@@ -41,7 +43,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
cx.background_executor().spawn(async move {
|
||||
self.extension
|
||||
.call({
|
||||
@@ -57,7 +59,16 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
|
||||
anyhow::Ok(completions)
|
||||
anyhow::Ok(
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|completion| ArgumentCompletion {
|
||||
label: completion.label,
|
||||
new_text: completion.new_text,
|
||||
run_command: completion.run_command,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ use release_channel::ReleaseChannel;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use snippet_provider::SnippetRegistry;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::str::FromStr;
|
||||
use std::{
|
||||
@@ -115,6 +116,7 @@ pub struct ExtensionStore {
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
indexed_docs_registry: Arc<IndexedDocsRegistry>,
|
||||
snippet_registry: Arc<SnippetRegistry>,
|
||||
modified_extensions: HashSet<Arc<str>>,
|
||||
wasm_host: Arc<WasmHost>,
|
||||
wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
|
||||
@@ -193,6 +195,7 @@ pub fn init(
|
||||
theme_registry,
|
||||
SlashCommandRegistry::global(cx),
|
||||
IndexedDocsRegistry::global(cx),
|
||||
SnippetRegistry::global(cx),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -227,6 +230,7 @@ impl ExtensionStore {
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
indexed_docs_registry: Arc<IndexedDocsRegistry>,
|
||||
snippet_registry: Arc<SnippetRegistry>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let work_dir = extensions_dir.join("work");
|
||||
@@ -259,6 +263,7 @@ impl ExtensionStore {
|
||||
theme_registry,
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
reload_tx,
|
||||
tasks: Vec::new(),
|
||||
};
|
||||
@@ -1045,6 +1050,7 @@ impl ExtensionStore {
|
||||
.collect::<Vec<_>>();
|
||||
let mut grammars_to_add = Vec::new();
|
||||
let mut themes_to_add = Vec::new();
|
||||
let mut snippets_to_add = Vec::new();
|
||||
for extension_id in &extensions_to_load {
|
||||
let Some(extension) = new_index.extensions.get(extension_id) else {
|
||||
continue;
|
||||
@@ -1062,6 +1068,11 @@ impl ExtensionStore {
|
||||
path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
|
||||
path
|
||||
}));
|
||||
snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| {
|
||||
let mut path = self.installed_dir.clone();
|
||||
path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]);
|
||||
path
|
||||
}));
|
||||
}
|
||||
|
||||
self.language_registry
|
||||
@@ -1097,6 +1108,7 @@ impl ExtensionStore {
|
||||
let wasm_host = self.wasm_host.clone();
|
||||
let root_dir = self.installed_dir.clone();
|
||||
let theme_registry = self.theme_registry.clone();
|
||||
let snippet_registry = self.snippet_registry.clone();
|
||||
let extension_entries = extensions_to_load
|
||||
.iter()
|
||||
.filter_map(|name| new_index.extensions.get(name).cloned())
|
||||
@@ -1117,6 +1129,15 @@ impl ExtensionStore {
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
for snippets_path in &snippets_to_add {
|
||||
if let Some(snippets_contents) = fs.load(snippets_path).await.log_err()
|
||||
{
|
||||
snippet_registry
|
||||
.register_snippets(snippets_path, &snippets_contents)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -19,6 +19,7 @@ use parking_lot::Mutex;
|
||||
use project::{Project, DEFAULT_COMPLETION_CONTEXT};
|
||||
use serde_json::json;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use snippet_provider::SnippetRegistry;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
@@ -160,6 +161,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
language_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -185,6 +187,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
language_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -258,6 +261,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
|
||||
let snippet_registry = Arc::new(SnippetRegistry::new());
|
||||
let node_runtime = FakeNodeRuntime::new();
|
||||
|
||||
let store = cx.new_model(|cx| {
|
||||
@@ -272,6 +276,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
theme_registry.clone(),
|
||||
slash_command_registry.clone(),
|
||||
indexed_docs_registry.clone(),
|
||||
snippet_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -345,6 +350,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
language_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -396,6 +402,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
theme_registry.clone(),
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -477,6 +484,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
|
||||
let snippet_registry = Arc::new(SnippetRegistry::new());
|
||||
let node_runtime = FakeNodeRuntime::new();
|
||||
|
||||
let mut status_updates = language_registry.language_server_binary_statuses();
|
||||
@@ -568,6 +576,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
theme_registry.clone(),
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ use wasmtime::{
|
||||
pub use latest::CodeLabelSpanLiteral;
|
||||
pub use latest::{
|
||||
zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind},
|
||||
zed::extension::slash_command::SlashCommandOutput,
|
||||
zed::extension::slash_command::{SlashCommandArgumentCompletion, SlashCommandOutput},
|
||||
CodeLabel, CodeLabelSpan, Command, Range, SlashCommand,
|
||||
};
|
||||
pub use since_v0_0_4::LanguageServerConfig;
|
||||
@@ -263,7 +263,7 @@ impl Extension {
|
||||
store: &mut Store<WasmState>,
|
||||
command: &SlashCommand,
|
||||
query: &str,
|
||||
) -> Result<Result<Vec<String>, String>> {
|
||||
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
|
||||
match self {
|
||||
Extension::V007(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, query)
|
||||
|
||||
@@ -421,27 +421,10 @@ impl ExtensionImports for WasmState {
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Zip => {
|
||||
let file_name = destination_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid download path"))?
|
||||
.to_string_lossy();
|
||||
let zip_filename = format!("{file_name}.zip");
|
||||
let mut zip_path = destination_path.clone();
|
||||
zip_path.set_file_name(zip_filename);
|
||||
|
||||
futures::pin_mut!(body);
|
||||
self.host.fs.create_file_with(&zip_path, body).await?;
|
||||
|
||||
let unzip_status = std::process::Command::new("unzip")
|
||||
.current_dir(&extension_work_dir)
|
||||
.arg("-d")
|
||||
.arg(&destination_path)
|
||||
.arg(&zip_path)
|
||||
.output()?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip {} archive", path.display()))?;
|
||||
}
|
||||
node_runtime::extract_zip(&destination_path, body)
|
||||
.await
|
||||
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ pub use wit::{
|
||||
npm_package_latest_version,
|
||||
},
|
||||
zed::extension::platform::{current_platform, Architecture, Os},
|
||||
zed::extension::slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection},
|
||||
zed::extension::slash_command::{
|
||||
SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, SlashCommandOutputSection,
|
||||
},
|
||||
CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars,
|
||||
KeyValueStore, LanguageServerInstallationStatus, Range, Worktree,
|
||||
};
|
||||
@@ -114,7 +116,7 @@ pub trait Extension: Send + Sync {
|
||||
&self,
|
||||
_command: SlashCommand,
|
||||
_query: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
) -> Result<Vec<SlashCommandArgumentCompletion>, String> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
@@ -247,7 +249,7 @@ impl wit::Guest for Component {
|
||||
fn complete_slash_command_argument(
|
||||
command: SlashCommand,
|
||||
query: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
) -> Result<Vec<SlashCommandArgumentCompletion>, String> {
|
||||
extension().complete_slash_command_argument(command, query)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ world extension {
|
||||
|
||||
use common.{range};
|
||||
use lsp.{completion, symbol};
|
||||
use slash-command.{slash-command, slash-command-output};
|
||||
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
|
||||
|
||||
/// Initializes the extension.
|
||||
export init-extension: func();
|
||||
@@ -130,7 +130,7 @@ world extension {
|
||||
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
|
||||
|
||||
/// Returns the completions that should be shown when completing the provided slash command with the given query.
|
||||
export complete-slash-command-argument: func(command: slash-command, query: string) -> result<list<string>, string>;
|
||||
export complete-slash-command-argument: func(command: slash-command, query: string) -> result<list<slash-command-argument-completion>, string>;
|
||||
|
||||
/// Returns the output from running the provided slash command.
|
||||
export run-slash-command: func(command: slash-command, argument: option<string>, worktree: borrow<worktree>) -> result<slash-command-output, string>;
|
||||
|
||||
@@ -28,4 +28,14 @@ interface slash-command {
|
||||
/// The label to display in the placeholder for this section.
|
||||
label: string,
|
||||
}
|
||||
|
||||
/// A completion for a slash command argument.
|
||||
record slash-command-argument-completion {
|
||||
/// The label to display for this completion.
|
||||
label: string,
|
||||
/// The new text that should be inserted into the command when this completion is accepted.
|
||||
new-text: string,
|
||||
/// Whether the command should be run when accepting this completion.
|
||||
run-command: bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ wayland-protocols = { version = "0.31.2", features = [
|
||||
] }
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
|
||||
oo7 = "0.3.0"
|
||||
open = "5.1.2"
|
||||
filedescriptor = "0.8.2"
|
||||
x11rb = { version = "0.13.0", features = [
|
||||
"allow-unsafe-code",
|
||||
@@ -142,7 +141,6 @@ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca
|
||||
] }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] }
|
||||
x11-clipboard = "0.9.2"
|
||||
mio = { version = "1.0.0", features = ["os-poll", "os-ext"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::env;
|
||||
|
||||
fn main() {
|
||||
let target = env::var("CARGO_CFG_TARGET_OS");
|
||||
|
||||
println!("cargo::rustc-check-cfg=cfg(gles)");
|
||||
match target.as_deref() {
|
||||
Ok("macos") => {
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -26,6 +26,8 @@ struct TextInput {
|
||||
selection_reversed: bool,
|
||||
marked_range: Option<Range<usize>>,
|
||||
last_layout: Option<ShapedLine>,
|
||||
last_bounds: Option<Bounds<Pixels>>,
|
||||
is_selecting: bool,
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
@@ -80,6 +82,21 @@ impl TextInput {
|
||||
self.replace_text_in_range(None, "", cx)
|
||||
}
|
||||
|
||||
fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut ViewContext<Self>) {
|
||||
self.is_selecting = true;
|
||||
self.move_to(self.index_for_mouse_position(event.position), cx)
|
||||
}
|
||||
|
||||
fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut ViewContext<Self>) {
|
||||
self.is_selecting = false;
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, event: &MouseMoveEvent, cx: &mut ViewContext<Self>) {
|
||||
if self.is_selecting {
|
||||
self.select_to(self.index_for_mouse_position(event.position), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
|
||||
cx.show_character_palette();
|
||||
}
|
||||
@@ -97,6 +114,20 @@ impl TextInput {
|
||||
}
|
||||
}
|
||||
|
||||
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
|
||||
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
if position.y < bounds.top() {
|
||||
return 0;
|
||||
}
|
||||
if position.y > bounds.bottom() {
|
||||
return self.content.len();
|
||||
}
|
||||
line.closest_index_for_x(position.x - bounds.left())
|
||||
}
|
||||
|
||||
fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start = offset
|
||||
@@ -409,6 +440,7 @@ impl Element for TextElement {
|
||||
}
|
||||
self.input.update(cx, |input, _cx| {
|
||||
input.last_layout = Some(line);
|
||||
input.last_bounds = Some(bounds);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -419,6 +451,7 @@ impl Render for TextInput {
|
||||
.flex()
|
||||
.key_context("TextInput")
|
||||
.track_focus(&self.focus_handle)
|
||||
.cursor(CursorStyle::IBeam)
|
||||
.on_action(cx.listener(Self::backspace))
|
||||
.on_action(cx.listener(Self::delete))
|
||||
.on_action(cx.listener(Self::left))
|
||||
@@ -429,6 +462,10 @@ impl Render for TextInput {
|
||||
.on_action(cx.listener(Self::home))
|
||||
.on_action(cx.listener(Self::end))
|
||||
.on_action(cx.listener(Self::show_character_palette))
|
||||
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
.on_mouse_move(cx.listener(Self::on_mouse_move))
|
||||
.bg(rgb(0xeeeeee))
|
||||
.size_full()
|
||||
.line_height(px(30.))
|
||||
@@ -446,6 +483,39 @@ impl Render for TextInput {
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for TextInput {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct InputExample {
|
||||
text_input: View<TextInput>,
|
||||
recent_keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
impl Render for InputExample {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.bg(rgb(0xaaaaaa))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.child(self.text_input.clone())
|
||||
.children(self.recent_keystrokes.iter().rev().map(|ks| {
|
||||
format!(
|
||||
"{:} {}",
|
||||
ks,
|
||||
if let Some(ime_key) = ks.ime_key.as_ref() {
|
||||
format!("-> {}", ime_key)
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
@@ -468,21 +538,36 @@ fn main() {
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|cx| TextInput {
|
||||
let text_input = cx.new_view(|cx| TextInput {
|
||||
focus_handle: cx.focus_handle(),
|
||||
content: "".into(),
|
||||
selected_range: 0..0,
|
||||
selection_reversed: false,
|
||||
marked_range: None,
|
||||
last_layout: None,
|
||||
last_bounds: None,
|
||||
is_selecting: false,
|
||||
});
|
||||
cx.new_view(|_| InputExample {
|
||||
text_input,
|
||||
recent_keystrokes: vec![],
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
cx.observe_keystrokes(move |ev, cx| {
|
||||
window
|
||||
.update(cx, |view, cx| {
|
||||
view.recent_keystrokes.push(ev.keystroke.clone());
|
||||
cx.notify();
|
||||
})
|
||||
.unwrap();
|
||||
})
|
||||
.detach();
|
||||
window
|
||||
.update(cx, |view, cx| {
|
||||
view.focus_handle.focus(cx);
|
||||
cx.activate(true)
|
||||
cx.focus_view(&view.text_input);
|
||||
cx.activate(true);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
@@ -2,63 +2,209 @@ use gpui::*;
|
||||
|
||||
struct WindowContent {
|
||||
text: SharedString,
|
||||
bounds: Bounds<Pixels>,
|
||||
bg: Hsla,
|
||||
}
|
||||
|
||||
impl Render for WindowContent {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let window_bounds = cx.bounds();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.bg(rgb(0x1e2025))
|
||||
.flex_col()
|
||||
.bg(self.bg)
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(self.text.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_sm()
|
||||
.items_center()
|
||||
.size_full()
|
||||
.child(format!(
|
||||
"origin: {}, {} size: {}, {}",
|
||||
self.bounds.origin.x,
|
||||
self.bounds.origin.y,
|
||||
self.bounds.size.width,
|
||||
self.bounds.size.height
|
||||
))
|
||||
.child(format!(
|
||||
"cx.bounds() origin: {}, {} size {}, {}",
|
||||
window_bounds.origin.x,
|
||||
window_bounds.origin.y,
|
||||
window_bounds.size.width,
|
||||
window_bounds.size.height
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> WindowOptions {
|
||||
WindowOptions {
|
||||
// Set the bounds of the window in screen coordinates
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
// Specify the display_id to ensure the window is created on the correct screen
|
||||
display_id: Some(display_id),
|
||||
titlebar: None,
|
||||
window_background: WindowBackgroundAppearance::Transparent,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
// Create several new windows, positioned in the top right corner of each screen
|
||||
let size = Size {
|
||||
width: px(350.),
|
||||
height: px(75.),
|
||||
};
|
||||
let margin_offset = px(150.);
|
||||
|
||||
for screen in cx.displays() {
|
||||
let options = {
|
||||
let margin_right = px(16.);
|
||||
let margin_height = px(-48.);
|
||||
|
||||
let size = Size {
|
||||
width: px(400.),
|
||||
height: px(72.),
|
||||
};
|
||||
|
||||
let bounds = gpui::Bounds::<Pixels> {
|
||||
origin: screen.bounds().upper_right()
|
||||
- point(size.width + margin_right, margin_height),
|
||||
size,
|
||||
};
|
||||
|
||||
WindowOptions {
|
||||
// Set the bounds of the window in screen coordinates
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
// Specify the display_id to ensure the window is created on the correct screen
|
||||
display_id: Some(screen.id()),
|
||||
|
||||
titlebar: None,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
}
|
||||
let bounds = Bounds {
|
||||
origin: point(margin_offset, margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(options, |cx| {
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("{:?}", screen.id()).into(),
|
||||
text: format!("Top Left {:?}", screen.id()).into(),
|
||||
bg: gpui::red(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().upper_right()
|
||||
- point(size.width + margin_offset, -margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Top Right {:?}", screen.id()).into(),
|
||||
bg: gpui::red(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().lower_left()
|
||||
- point(-margin_offset, size.height + margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Bottom Left {:?}", screen.id()).into(),
|
||||
bg: gpui::blue(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().lower_right()
|
||||
- point(size.width + margin_offset, size.height + margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Bottom Right {:?}", screen.id()).into(),
|
||||
bg: gpui::blue(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(screen.bounds().center().x - size.center().x, margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Top Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(margin_offset, screen.bounds().center().y - size.center().y),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Left Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().center().x - size.center().x,
|
||||
screen.bounds().center().y - size.center().y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().size.width - size.width - margin_offset,
|
||||
screen.bounds().center().y - size.center().y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Right Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().center().x - size.center().x,
|
||||
screen.bounds().size.height - size.height - margin_offset,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Bottom Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -2320,6 +2320,18 @@ impl Pixels {
|
||||
Self(self.0.abs())
|
||||
}
|
||||
|
||||
/// Returns the sign of the `Pixels` value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns:
|
||||
/// * `1.0` if the value is positive
|
||||
/// * `-1.0` if the value is negative
|
||||
/// * `0.0` if the value is zero
|
||||
pub fn signum(&self) -> f32 {
|
||||
self.0.signum()
|
||||
}
|
||||
|
||||
/// Returns the f64 value of `Pixels`.
|
||||
///
|
||||
/// # Returns
|
||||
|
||||
@@ -291,14 +291,41 @@ impl ScrollDelta {
|
||||
}
|
||||
|
||||
/// Combines two scroll deltas into one.
|
||||
/// If the signs of the deltas are the same (both positive or both negative),
|
||||
/// the deltas are added together. If the signs are opposite, the second delta
|
||||
/// (other) is used, effectively overriding the first delta.
|
||||
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
|
||||
match (self, other) {
|
||||
(ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
|
||||
ScrollDelta::Pixels(px_a + px_b)
|
||||
(ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => {
|
||||
let x = if a.x.signum() * b.x.signum() >= 0. {
|
||||
a.x + b.x
|
||||
} else {
|
||||
b.x
|
||||
};
|
||||
|
||||
let y = if a.y.signum() * b.y.signum() >= 0. {
|
||||
a.y + b.y
|
||||
} else {
|
||||
b.y
|
||||
};
|
||||
|
||||
ScrollDelta::Pixels(point(x, y))
|
||||
}
|
||||
|
||||
(ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => {
|
||||
ScrollDelta::Lines(lines_a + lines_b)
|
||||
(ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => {
|
||||
let x = if a.x.signum() * b.x.signum() >= 0. {
|
||||
a.x + b.x
|
||||
} else {
|
||||
b.x
|
||||
};
|
||||
|
||||
let y = if a.y.signum() * b.y.signum() >= 0. {
|
||||
a.y + b.y
|
||||
} else {
|
||||
b.y
|
||||
};
|
||||
|
||||
ScrollDelta::Lines(point(x, y))
|
||||
}
|
||||
|
||||
_ => other,
|
||||
|
||||
@@ -281,6 +281,16 @@ pub struct Tiling {
|
||||
}
|
||||
|
||||
impl Tiling {
|
||||
/// Initializes a [`Tiling`] type with all sides tiled
|
||||
pub fn tiled() -> Self {
|
||||
Self {
|
||||
top: true,
|
||||
left: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether any edge is tiled
|
||||
pub fn is_tiled(&self) -> bool {
|
||||
self.top || self.left || self.right || self.bottom
|
||||
@@ -308,6 +318,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
) -> Option<oneshot::Receiver<usize>>;
|
||||
fn activate(&self);
|
||||
fn is_active(&self) -> bool;
|
||||
fn is_hovered(&self) -> bool;
|
||||
fn set_title(&mut self, title: &str);
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
|
||||
fn minimize(&self);
|
||||
@@ -317,6 +328,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
|
||||
|
||||
@@ -5,10 +5,12 @@ use calloop::{
|
||||
timer::TimeoutAction,
|
||||
EventLoop,
|
||||
};
|
||||
use mio::Waker;
|
||||
use parking::{Parker, Unparker};
|
||||
use parking_lot::Mutex;
|
||||
use std::{sync::Arc, thread, time::Duration};
|
||||
use std::{
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
struct TimerAfter {
|
||||
@@ -19,7 +21,6 @@ struct TimerAfter {
|
||||
pub(crate) struct LinuxDispatcher {
|
||||
parker: Mutex<Parker>,
|
||||
main_sender: Sender<Runnable>,
|
||||
main_waker: Option<Arc<Waker>>,
|
||||
timer_sender: Sender<TimerAfter>,
|
||||
background_sender: flume::Sender<Runnable>,
|
||||
_background_threads: Vec<thread::JoinHandle<()>>,
|
||||
@@ -27,18 +28,26 @@ pub(crate) struct LinuxDispatcher {
|
||||
}
|
||||
|
||||
impl LinuxDispatcher {
|
||||
pub fn new(main_sender: Sender<Runnable>, main_waker: Option<Arc<Waker>>) -> Self {
|
||||
pub fn new(main_sender: Sender<Runnable>) -> Self {
|
||||
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
|
||||
let thread_count = std::thread::available_parallelism()
|
||||
.map(|i| i.get())
|
||||
.unwrap_or(1);
|
||||
|
||||
let mut background_threads = (0..thread_count)
|
||||
.map(|_| {
|
||||
.map(|i| {
|
||||
let receiver = background_receiver.clone();
|
||||
std::thread::spawn(move || {
|
||||
for runnable in receiver {
|
||||
let start = Instant::now();
|
||||
|
||||
runnable.run();
|
||||
|
||||
log::trace!(
|
||||
"background thread {}: ran runnable. took: {:?}",
|
||||
i,
|
||||
start.elapsed()
|
||||
);
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -79,7 +88,6 @@ impl LinuxDispatcher {
|
||||
Self {
|
||||
parker: Mutex::new(Parker::new()),
|
||||
main_sender,
|
||||
main_waker,
|
||||
timer_sender,
|
||||
background_sender,
|
||||
_background_threads: background_threads,
|
||||
@@ -99,9 +107,6 @@ impl PlatformDispatcher for LinuxDispatcher {
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||
self.main_sender.send(runnable).ok();
|
||||
if let Some(main_waker) = self.main_waker.as_ref() {
|
||||
main_waker.wake().ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
|
||||
|
||||
@@ -22,7 +22,7 @@ impl HeadlessClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
@@ -81,6 +81,8 @@ impl LinuxClient for HeadlessClient {
|
||||
|
||||
fn open_uri(&self, _uri: &str) {}
|
||||
|
||||
fn reveal_path(&self, _path: std::path::PathBuf) {}
|
||||
|
||||
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::os::fd::{AsRawFd, FromRawFd};
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
|
||||
use std::panic::Location;
|
||||
use std::rc::Weak;
|
||||
use std::{
|
||||
@@ -20,13 +20,14 @@ use std::{
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
|
||||
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
|
||||
use ashpd::{url, ActivationToken};
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventLoop, LoopHandle, LoopSignal};
|
||||
use filedescriptor::FileDescriptor;
|
||||
use flume::{Receiver, Sender};
|
||||
use futures::channel::oneshot;
|
||||
use mio::Waker;
|
||||
use parking_lot::Mutex;
|
||||
use time::UtcOffset;
|
||||
use util::ResultExt;
|
||||
@@ -67,6 +68,7 @@ pub trait LinuxClient {
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>>;
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn open_uri(&self, uri: &str);
|
||||
fn reveal_path(&self, path: PathBuf);
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
@@ -85,16 +87,6 @@ pub(crate) struct PlatformHandlers {
|
||||
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
}
|
||||
|
||||
pub trait QuitSignal {
|
||||
fn quit(&mut self);
|
||||
}
|
||||
|
||||
impl QuitSignal for LoopSignal {
|
||||
fn quit(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LinuxCommon {
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
@@ -102,20 +94,17 @@ pub(crate) struct LinuxCommon {
|
||||
pub(crate) appearance: WindowAppearance,
|
||||
pub(crate) auto_hide_scrollbars: bool,
|
||||
pub(crate) callbacks: PlatformHandlers,
|
||||
pub(crate) quit_signal: Box<dyn QuitSignal>,
|
||||
pub(crate) signal: LoopSignal,
|
||||
pub(crate) menus: Vec<OwnedMenu>,
|
||||
}
|
||||
|
||||
impl LinuxCommon {
|
||||
pub fn new(
|
||||
quit_signal: Box<dyn QuitSignal>,
|
||||
main_waker: Option<Arc<Waker>>,
|
||||
) -> (Self, Channel<Runnable>) {
|
||||
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
|
||||
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
|
||||
let text_system = Arc::new(CosmicTextSystem::new());
|
||||
let callbacks = PlatformHandlers::default();
|
||||
|
||||
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone(), main_waker));
|
||||
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
|
||||
|
||||
let background_executor = BackgroundExecutor::new(dispatcher.clone());
|
||||
|
||||
@@ -126,7 +115,7 @@ impl LinuxCommon {
|
||||
appearance: WindowAppearance::Light,
|
||||
auto_hide_scrollbars: false,
|
||||
callbacks,
|
||||
quit_signal,
|
||||
signal,
|
||||
menus: Vec::new(),
|
||||
};
|
||||
|
||||
@@ -160,7 +149,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
self.with_common(|common| common.quit_signal.quit());
|
||||
self.with_common(|common| common.signal.stop());
|
||||
}
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
@@ -314,20 +303,27 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
let directory = directory.to_owned();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
let result = SaveFileRequest::default()
|
||||
let request = SaveFileRequest::default()
|
||||
.modal(true)
|
||||
.title("Select new path")
|
||||
.accept_label("Accept")
|
||||
.send()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|request| request.response().ok())
|
||||
.and_then(|response| {
|
||||
response
|
||||
.uris()
|
||||
.first()
|
||||
.and_then(|uri| uri.to_file_path().ok())
|
||||
});
|
||||
.current_folder(directory);
|
||||
|
||||
let result = if let Ok(request) = request {
|
||||
request
|
||||
.send()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|request| request.response().ok())
|
||||
.and_then(|response| {
|
||||
response
|
||||
.uris()
|
||||
.first()
|
||||
.and_then(|uri| uri.to_file_path().ok())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
done_tx.send(result);
|
||||
})
|
||||
@@ -337,13 +333,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
if path.is_dir() {
|
||||
open::that_detached(path);
|
||||
return;
|
||||
}
|
||||
// If `path` is a file, the system may try to open it in a text editor
|
||||
let dir = path.parent().unwrap_or(Path::new(""));
|
||||
open::that_detached(dir);
|
||||
self.reveal_path(path.to_owned());
|
||||
}
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>) {
|
||||
@@ -504,18 +494,40 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
fn add_recent_document(&self, _path: &Path) {}
|
||||
}
|
||||
|
||||
pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) {
|
||||
let mut last_err = None;
|
||||
for mut command in open::commands(uri) {
|
||||
if let Some(token) = activation_token {
|
||||
command.env("XDG_ACTIVATION_TOKEN", token);
|
||||
}
|
||||
match command.spawn() {
|
||||
Ok(_) => return,
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
pub(super) fn open_uri_internal(
|
||||
executor: BackgroundExecutor,
|
||||
uri: &str,
|
||||
activation_token: Option<String>,
|
||||
) {
|
||||
if let Some(uri) = url::Url::parse(uri).log_err() {
|
||||
executor
|
||||
.spawn(async move {
|
||||
OpenUriRequest::default()
|
||||
.activation_token(activation_token.map(ActivationToken::from))
|
||||
.send_uri(&uri)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
log::error!("failed to open uri: {uri:?}, last error: {last_err:?}");
|
||||
}
|
||||
|
||||
pub(super) fn reveal_path_internal(
|
||||
executor: BackgroundExecutor,
|
||||
path: PathBuf,
|
||||
activation_token: Option<String>,
|
||||
) {
|
||||
executor
|
||||
.spawn(async move {
|
||||
if let Some(dir) = File::open(path).log_err() {
|
||||
OpenDirectoryRequest::default()
|
||||
.activation_token(activation_token.map(ActivationToken::from))
|
||||
.send(&dir.as_fd())
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
|
||||
@@ -628,6 +640,8 @@ impl Keystroke {
|
||||
Keysym::Prior => "pageup".to_owned(),
|
||||
Keysym::Next => "pagedown".to_owned(),
|
||||
Keysym::ISO_Left_Tab => "tab".to_owned(),
|
||||
Keysym::KP_Prior => "pageup".to_owned(),
|
||||
Keysym::KP_Next => "pagedown".to_owned(),
|
||||
|
||||
Keysym::comma => ",".to_owned(),
|
||||
Keysym::period => ".".to_owned(),
|
||||
@@ -665,7 +679,14 @@ impl Keystroke {
|
||||
Keysym::equal => "=".to_owned(),
|
||||
Keysym::plus => "+".to_owned(),
|
||||
|
||||
_ => xkb::keysym_get_name(key_sym).to_lowercase(),
|
||||
_ => {
|
||||
let name = xkb::keysym_get_name(key_sym).to_lowercase();
|
||||
if key_sym.is_keypad_key() {
|
||||
name.replace("kp_", "")
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if modifiers.shift {
|
||||
|
||||
@@ -61,7 +61,6 @@ use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blu
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
|
||||
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
|
||||
use super::display::WaylandDisplay;
|
||||
use super::window::{ImeInput, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::wayland::clipboard::{
|
||||
@@ -72,11 +71,14 @@ use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
use crate::platform::linux::wayland::window::WaylandWindow;
|
||||
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::linux::{get_xkb_compose_state, is_within_click_distance};
|
||||
use crate::platform::linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
|
||||
reveal_path_internal,
|
||||
};
|
||||
use crate::platform::PlatformWindow;
|
||||
use crate::{
|
||||
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
|
||||
SCROLL_LINES,
|
||||
DOUBLE_CLICK_INTERVAL, SCROLL_LINES,
|
||||
};
|
||||
use crate::{
|
||||
AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
|
||||
@@ -220,7 +222,7 @@ pub(crate) struct WaylandClientState {
|
||||
data_offers: Vec<DataOffer<WlDataOffer>>,
|
||||
primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
cursor: Cursor,
|
||||
pending_open_uri: Option<String>,
|
||||
pending_activation: Option<PendingActivation>,
|
||||
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
|
||||
common: LinuxCommon,
|
||||
}
|
||||
@@ -244,6 +246,15 @@ pub(crate) struct KeyRepeat {
|
||||
current_keycode: Option<xkb::Keycode>,
|
||||
}
|
||||
|
||||
pub(crate) enum PendingActivation {
|
||||
/// URI to open in the web browser.
|
||||
Uri(String),
|
||||
/// Path to open in the file explorer.
|
||||
Path(PathBuf),
|
||||
/// A window from ourselves to raise.
|
||||
Window(ObjectId),
|
||||
}
|
||||
|
||||
/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the
|
||||
/// window to GPUI.
|
||||
#[derive(Clone)]
|
||||
@@ -260,6 +271,11 @@ impl WaylandClientStatePtr {
|
||||
self.0.upgrade().unwrap().borrow().serial_tracker.get(kind)
|
||||
}
|
||||
|
||||
pub fn set_pending_activation(&self, window: ObjectId) {
|
||||
self.0.upgrade().unwrap().borrow_mut().pending_activation =
|
||||
Some(PendingActivation::Window(window));
|
||||
}
|
||||
|
||||
pub fn enable_ime(&self) {
|
||||
let client = self.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@@ -310,7 +326,7 @@ impl WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
if state.windows.is_empty() {
|
||||
state.common.quit_signal.quit();
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,7 +422,7 @@ impl WaylandClient {
|
||||
|
||||
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
handle
|
||||
@@ -443,7 +459,7 @@ impl WaylandClient {
|
||||
let mut cursor = Cursor::new(&conn, &globals, 24);
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor, None), {
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
if let Some(client) = client.0.upgrade() {
|
||||
@@ -530,7 +546,7 @@ impl WaylandClient {
|
||||
data_offers: Vec::new(),
|
||||
primary_data_offer: None,
|
||||
cursor,
|
||||
pending_open_uri: None,
|
||||
pending_activation: None,
|
||||
event_loop: Some(event_loop),
|
||||
}));
|
||||
|
||||
@@ -629,14 +645,33 @@ impl LinuxClient for WaylandClient {
|
||||
state.globals.activation.clone(),
|
||||
state.mouse_focused_window.clone(),
|
||||
) {
|
||||
state.pending_open_uri = Some(uri.to_owned());
|
||||
state.pending_activation = Some(PendingActivation::Uri(uri.to_string()));
|
||||
let token = activation.get_activation_token(&state.globals.qh, ());
|
||||
let serial = state.serial_tracker.get(SerialKind::MousePress);
|
||||
token.set_serial(serial, &state.wl_seat);
|
||||
token.set_surface(&window.surface());
|
||||
token.commit();
|
||||
} else {
|
||||
open_uri_internal(uri, None);
|
||||
let executor = state.common.background_executor.clone();
|
||||
open_uri_internal(executor, uri, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: PathBuf) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if let (Some(activation), Some(window)) = (
|
||||
state.globals.activation.clone(),
|
||||
state.mouse_focused_window.clone(),
|
||||
) {
|
||||
state.pending_activation = Some(PendingActivation::Path(path));
|
||||
let token = activation.get_activation_token(&state.globals.qh, ());
|
||||
let serial = state.serial_tracker.get(SerialKind::MousePress);
|
||||
token.set_serial(serial, &state.wl_seat);
|
||||
token.set_surface(&window.surface());
|
||||
token.commit();
|
||||
} else {
|
||||
let executor = state.common.background_executor.clone();
|
||||
reveal_path_internal(executor, path, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,7 +844,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
|
||||
match event {
|
||||
wl_callback::Event::Done { .. } => {
|
||||
window.frame(true);
|
||||
window.frame();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -954,13 +989,25 @@ impl Dispatch<xdg_activation_token_v1::XdgActivationTokenV1, ()> for WaylandClie
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
if let xdg_activation_token_v1::Event::Done { token } = event {
|
||||
if let Some(uri) = state.pending_open_uri.take() {
|
||||
open_uri_internal(&uri, Some(&token));
|
||||
} else {
|
||||
log::error!("called while pending_open_uri is None");
|
||||
let executor = state.common.background_executor.clone();
|
||||
match state.pending_activation.take() {
|
||||
Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)),
|
||||
Some(PendingActivation::Path(path)) => {
|
||||
reveal_path_internal(executor, path, Some(token))
|
||||
}
|
||||
Some(PendingActivation::Window(window)) => {
|
||||
let Some(window) = get_window(&mut state, &window) else {
|
||||
return;
|
||||
};
|
||||
let activation = state.globals.activation.as_ref().unwrap();
|
||||
activation.activate(token, &window.surface());
|
||||
}
|
||||
None => log::error!("activation token received with no pending activation"),
|
||||
}
|
||||
}
|
||||
|
||||
token.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1194,7 +1241,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
&& state.repeat.current_keycode.is_some()
|
||||
&& state.keyboard_focused_window.is_some();
|
||||
|
||||
if !is_repeating {
|
||||
if !is_repeating || rate == 0 {
|
||||
return TimeoutAction::Drop;
|
||||
}
|
||||
|
||||
@@ -1356,6 +1403,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
|
||||
if let Some(window) = get_window(&mut state, &surface.id()) {
|
||||
state.mouse_focused_window = Some(window.clone());
|
||||
|
||||
if state.enter_token.is_some() {
|
||||
state.enter_token = None;
|
||||
}
|
||||
@@ -1369,7 +1417,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
window.set_focused(true);
|
||||
window.set_hovered(true);
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Leave { .. } => {
|
||||
@@ -1385,7 +1433,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
|
||||
drop(state);
|
||||
focused_window.handle_input(input);
|
||||
focused_window.set_focused(false);
|
||||
focused_window.set_hovered(false);
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Motion {
|
||||
@@ -1508,6 +1556,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
if state.axis_source == AxisSource::Wheel {
|
||||
return;
|
||||
}
|
||||
let axis = if state.modifiers.shift {
|
||||
wl_pointer::Axis::HorizontalScroll
|
||||
} else {
|
||||
axis
|
||||
};
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
@@ -1533,6 +1586,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
discrete,
|
||||
} => {
|
||||
state.scroll_event_received = true;
|
||||
let axis = if state.modifiers.shift {
|
||||
wl_pointer::Axis::HorizontalScroll
|
||||
} else {
|
||||
axis
|
||||
};
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
@@ -1555,6 +1613,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
value120,
|
||||
} => {
|
||||
state.scroll_event_received = true;
|
||||
let axis = if state.modifiers.shift {
|
||||
wl_pointer::Axis::HorizontalScroll
|
||||
} else {
|
||||
axis
|
||||
};
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
|
||||
@@ -36,6 +36,7 @@ pub(crate) struct Callbacks {
|
||||
request_frame: Option<Box<dyn FnMut()>>,
|
||||
input: Option<Box<dyn FnMut(crate::PlatformInput) -> crate::DispatchEventResult>>,
|
||||
active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
hover_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
moved: Option<Box<dyn FnMut()>>,
|
||||
should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||
@@ -76,6 +77,7 @@ pub struct WaylandWindowState {
|
||||
acknowledged_first_configure: bool,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
app_id: Option<String>,
|
||||
appearance: WindowAppearance,
|
||||
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
@@ -96,11 +98,11 @@ pub struct WaylandWindowState {
|
||||
client: WaylandClientStatePtr,
|
||||
handle: AnyWindowHandle,
|
||||
active: bool,
|
||||
hovered: bool,
|
||||
in_progress_configure: Option<InProgressConfigure>,
|
||||
in_progress_window_controls: Option<WindowControls>,
|
||||
window_controls: WindowControls,
|
||||
inset: Option<Pixels>,
|
||||
requested_inset: Option<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -159,6 +161,7 @@ impl WaylandWindowState {
|
||||
acknowledged_first_configure: false,
|
||||
surface,
|
||||
decoration,
|
||||
app_id: None,
|
||||
blur: None,
|
||||
toplevel,
|
||||
viewport,
|
||||
@@ -180,6 +183,7 @@ impl WaylandWindowState {
|
||||
appearance,
|
||||
handle,
|
||||
active: false,
|
||||
hovered: false,
|
||||
in_progress_window_controls: None,
|
||||
// Assume that we can do anything, unless told otherwise
|
||||
window_controls: WindowControls {
|
||||
@@ -189,7 +193,6 @@ impl WaylandWindowState {
|
||||
window_menu: true,
|
||||
},
|
||||
inset: None,
|
||||
requested_inset: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -316,12 +319,11 @@ impl WaylandWindowStatePtr {
|
||||
Rc::ptr_eq(&self.state, &other.state)
|
||||
}
|
||||
|
||||
pub fn frame(&self, request_frame_callback: bool) {
|
||||
if request_frame_callback {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
drop(state);
|
||||
}
|
||||
pub fn frame(&self) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
drop(state);
|
||||
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = cb.request_frame.as_mut() {
|
||||
fun();
|
||||
@@ -351,13 +353,12 @@ impl WaylandWindowStatePtr {
|
||||
state.fullscreen = configure.fullscreen;
|
||||
state.maximized = configure.maximized;
|
||||
state.tiling = configure.tiling;
|
||||
if got_unmaximized {
|
||||
configure.size = Some(state.window_bounds.size);
|
||||
} else if !configure.maximized {
|
||||
configure.size =
|
||||
compute_outer_size(state.inset, configure.size, state.tiling);
|
||||
}
|
||||
if !configure.fullscreen && !configure.maximized {
|
||||
configure.size = if got_unmaximized {
|
||||
Some(state.window_bounds.size)
|
||||
} else {
|
||||
compute_outer_size(state.inset, configure.size, state.tiling)
|
||||
};
|
||||
if let Some(size) = configure.size {
|
||||
state.window_bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
@@ -373,12 +374,27 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.xdg_surface.ack_configure(serial);
|
||||
let request_frame_callback = !state.acknowledged_first_configure;
|
||||
state.acknowledged_first_configure = true;
|
||||
|
||||
let window_geometry = inset_by_tiling(
|
||||
state.bounds.map_origin(|_| px(0.0)),
|
||||
state.inset.unwrap_or(px(0.0)),
|
||||
state.tiling,
|
||||
)
|
||||
.map(|v| v.0 as i32)
|
||||
.map_size(|v| if v <= 0 { 1 } else { v });
|
||||
|
||||
state.xdg_surface.set_window_geometry(
|
||||
window_geometry.origin.x,
|
||||
window_geometry.origin.y,
|
||||
window_geometry.size.width,
|
||||
window_geometry.size.height,
|
||||
);
|
||||
|
||||
let request_frame_callback = !state.acknowledged_first_configure;
|
||||
if request_frame_callback {
|
||||
state.acknowledged_first_configure = true;
|
||||
drop(state);
|
||||
self.frame(true);
|
||||
self.frame();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -470,6 +486,10 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
if fullscreen || maximized {
|
||||
tiling = Tiling::tiled();
|
||||
}
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.in_progress_configure = Some(InProgressConfigure {
|
||||
size,
|
||||
@@ -683,6 +703,12 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_hovered(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().hover_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
|
||||
self.state.borrow_mut().appearance = appearance;
|
||||
|
||||
@@ -808,19 +834,38 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
log::info!("Wayland does not support this API");
|
||||
// Try to request an activation token. Even though the activation is likely going to be rejected,
|
||||
// KWin and Mutter can use the app_id to visually indicate we're requesting attention.
|
||||
let state = self.borrow();
|
||||
if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone())
|
||||
{
|
||||
state.client.set_pending_activation(state.surface.id());
|
||||
let token = activation.get_activation_token(&state.globals.qh, ());
|
||||
// The serial isn't exactly important here, since the activation is probably going to be rejected anyway.
|
||||
let serial = state.client.get_serial(SerialKind::MousePress);
|
||||
token.set_app_id(app_id);
|
||||
token.set_serial(serial, &state.globals.seat);
|
||||
token.set_surface(&state.surface);
|
||||
token.commit();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.borrow().active
|
||||
}
|
||||
|
||||
fn is_hovered(&self) -> bool {
|
||||
self.borrow().hovered
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.borrow().toplevel.set_title(title.to_string());
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
self.borrow().toplevel.set_app_id(app_id.to_owned());
|
||||
let mut state = self.borrow_mut();
|
||||
state.toplevel.set_app_id(app_id.to_owned());
|
||||
state.app_id = Some(app_id.to_owned());
|
||||
}
|
||||
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
@@ -867,6 +912,10 @@ impl PlatformWindow for WaylandWindow {
|
||||
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.callbacks.borrow_mut().hover_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
@@ -893,26 +942,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn completed_frame(&self) {
|
||||
let mut state = self.borrow_mut();
|
||||
if let Some(area) = state.requested_inset {
|
||||
state.inset = Some(area);
|
||||
}
|
||||
|
||||
let window_geometry = inset_by_tiling(
|
||||
state.bounds.map_origin(|_| px(0.0)),
|
||||
state.inset.unwrap_or(px(0.0)),
|
||||
state.tiling,
|
||||
)
|
||||
.map(|v| v.0 as i32)
|
||||
.map_size(|v| if v <= 0 { 1 } else { v });
|
||||
|
||||
state.xdg_surface.set_window_geometry(
|
||||
window_geometry.origin.x,
|
||||
window_geometry.origin.y,
|
||||
window_geometry.size.width,
|
||||
window_geometry.size.height,
|
||||
);
|
||||
|
||||
let state = self.borrow();
|
||||
state.surface.commit();
|
||||
}
|
||||
|
||||
@@ -973,7 +1003,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
fn set_client_inset(&self, inset: Pixels) {
|
||||
let mut state = self.borrow_mut();
|
||||
if Some(inset) != state.inset {
|
||||
state.requested_inset = Some(inset);
|
||||
state.inset = Some(inset);
|
||||
update_window(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Deref;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
use calloop::{EventLoop, LoopHandle, RegistrationToken};
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use mio::{Interest, Token, Waker};
|
||||
use util::ResultExt;
|
||||
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::cursor;
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::ConnectionExt;
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, KeyPressEvent};
|
||||
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
|
||||
use x11rb::resource_manager::Database;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
@@ -33,24 +30,24 @@ use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
|
||||
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, Platform, PlatformDisplay,
|
||||
PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{get_xkb_compose_state, open_uri_internal, SCROLL_LINES},
|
||||
X11Display, X11WindowStatePtr, XcbAtoms,
|
||||
};
|
||||
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
|
||||
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{XimCallbackEvent, XimHandler};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
|
||||
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
|
||||
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
|
||||
use crate::platform::linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
|
||||
};
|
||||
|
||||
pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
|
||||
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11WindowStatePtr,
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
impl WindowRef {
|
||||
@@ -98,25 +95,23 @@ impl From<xim::ClientError> for EventHandlerError {
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
/// poll is in an Option so we can take it out in `run()` without
|
||||
/// mutating self.
|
||||
poll: Option<mio::Poll>,
|
||||
quit_signal_rx: oneshot::Receiver<()>,
|
||||
runnables: Channel<Runnable>,
|
||||
xdp_event_source: XDPEventSource,
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
|
||||
pub(crate) last_click: Instant,
|
||||
pub(crate) last_location: Point<Pixels>,
|
||||
pub(crate) current_count: usize,
|
||||
|
||||
pub(crate) scale_factor: f32,
|
||||
|
||||
pub(crate) xcb_connection: Rc<XCBConnection>,
|
||||
client_side_decorations_supported: bool,
|
||||
pub(crate) x_root_index: usize,
|
||||
pub(crate) _resource_database: Database,
|
||||
pub(crate) atoms: XcbAtoms,
|
||||
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
|
||||
pub(crate) focused_window: Option<xproto::Window>,
|
||||
pub(crate) mouse_focused_window: Option<xproto::Window>,
|
||||
pub(crate) keyboard_focused_window: Option<xproto::Window>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
@@ -125,6 +120,7 @@ pub struct X11ClientState {
|
||||
pub(crate) compose_state: Option<xkbc::compose::State>,
|
||||
pub(crate) pre_edit_text: Option<String>,
|
||||
pub(crate) composing: bool,
|
||||
pub(crate) pre_ime_key_down: Option<Keystroke>,
|
||||
pub(crate) cursor_handle: cursor::Handle,
|
||||
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
|
||||
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
|
||||
@@ -146,46 +142,19 @@ impl X11ClientStatePtr {
|
||||
let client = X11Client(self.0.upgrade().expect("client already dropped"));
|
||||
let mut state = client.0.borrow_mut();
|
||||
|
||||
if state.windows.remove(&x_window).is_none() {
|
||||
log::warn!(
|
||||
"failed to remove X window {} from client state, does not exist",
|
||||
x_window
|
||||
);
|
||||
if let Some(window_ref) = state.windows.remove(&x_window) {
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
}
|
||||
if state.mouse_focused_window == Some(x_window) {
|
||||
state.mouse_focused_window = None;
|
||||
}
|
||||
if state.keyboard_focused_window == Some(x_window) {
|
||||
state.keyboard_focused_window = None;
|
||||
}
|
||||
|
||||
state.cursor_styles.remove(&x_window);
|
||||
|
||||
if state.windows.is_empty() {
|
||||
state.common.quit_signal.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelQuitSignal {
|
||||
tx: Option<oneshot::Sender<()>>,
|
||||
waker: Option<Arc<Waker>>,
|
||||
}
|
||||
|
||||
impl ChannelQuitSignal {
|
||||
fn new(waker: Option<Arc<Waker>>) -> (Self, oneshot::Receiver<()>) {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
|
||||
let quit_signal = ChannelQuitSignal {
|
||||
tx: Some(tx),
|
||||
waker,
|
||||
};
|
||||
|
||||
(quit_signal, rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl QuitSignal for ChannelQuitSignal {
|
||||
fn quit(&mut self) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(()).log_err();
|
||||
if let Some(waker) = self.waker.as_ref() {
|
||||
waker.wake().ok();
|
||||
}
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,12 +164,27 @@ pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11Client {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut poll = mio::Poll::new().unwrap();
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let waker = Arc::new(Waker::new(poll.registry(), WAKER_TOKEN).unwrap());
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new(Some(waker.clone()));
|
||||
let (common, runnables) = LinuxCommon::new(Box::new(quit_signal), Some(waker.clone()));
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle
|
||||
.insert_source(main_receiver, {
|
||||
let handle = handle.clone();
|
||||
move |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
|
||||
// events have higher priority and runnables are only worked off after the event
|
||||
// callbacks.
|
||||
handle.insert_idle(|_| {
|
||||
runnable.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
|
||||
xcb_connection
|
||||
@@ -311,18 +295,47 @@ impl X11Client {
|
||||
None
|
||||
};
|
||||
|
||||
let xdp_event_source =
|
||||
XDPEventSource::new(&common.background_executor, Some(waker.clone()));
|
||||
// Safety: Safe if xcb::Connection always returns a valid fd
|
||||
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
|
||||
|
||||
handle
|
||||
.insert_source(
|
||||
Generic::new_with_error::<EventHandlerError>(
|
||||
fd,
|
||||
calloop::Interest::READ,
|
||||
calloop::Mode::Level,
|
||||
),
|
||||
{
|
||||
let xcb_connection = xcb_connection.clone();
|
||||
move |_readiness, _, client| {
|
||||
client.process_x11_events(&xcb_connection)?;
|
||||
Ok(calloop::PostAction::Continue)
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("Failed to initialize x11 event source");
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
client.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut client.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
|
||||
// noop, X11 manages this for us.
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
poll: Some(poll),
|
||||
runnables,
|
||||
|
||||
xdp_event_source,
|
||||
quit_signal_rx,
|
||||
common,
|
||||
|
||||
modifiers: Modifiers::default(),
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
common,
|
||||
last_click: Instant::now(),
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
@@ -334,13 +347,15 @@ impl X11Client {
|
||||
_resource_database: resource_database,
|
||||
atoms,
|
||||
windows: HashMap::default(),
|
||||
focused_window: None,
|
||||
mouse_focused_window: None,
|
||||
keyboard_focused_window: None,
|
||||
xkb: xkb_state,
|
||||
ximc,
|
||||
xim_handler,
|
||||
|
||||
compose_state,
|
||||
pre_edit_text: None,
|
||||
pre_ime_key_down: None,
|
||||
composing: false,
|
||||
|
||||
cursor_handle,
|
||||
@@ -356,6 +371,125 @@ impl X11Client {
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn process_x11_events(
|
||||
&self,
|
||||
xcb_connection: &XCBConnection,
|
||||
) -> Result<(), EventHandlerError> {
|
||||
loop {
|
||||
let mut events = Vec::new();
|
||||
let mut windows_to_refresh = HashSet::new();
|
||||
|
||||
let mut last_key_release = None;
|
||||
let mut last_key_press: Option<KeyPressEvent> = None;
|
||||
|
||||
loop {
|
||||
match xcb_connection.poll_for_event() {
|
||||
Ok(Some(event)) => {
|
||||
match event {
|
||||
Event::Expose(expose_event) => {
|
||||
windows_to_refresh.insert(expose_event.window);
|
||||
}
|
||||
Event::KeyRelease(_) => {
|
||||
last_key_release = Some(event);
|
||||
}
|
||||
Event::KeyPress(key_press) => {
|
||||
if let Some(last_press) = last_key_press.as_ref() {
|
||||
if last_press.detail == key_press.detail {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Event::KeyRelease(key_release)) =
|
||||
last_key_release.take()
|
||||
{
|
||||
// We ignore that last KeyRelease if it's too close to this KeyPress,
|
||||
// suggesting that it's auto-generated by X11 as a key-repeat event.
|
||||
if key_release.detail != key_press.detail
|
||||
|| key_press.time.saturating_sub(key_release.time) > 20
|
||||
{
|
||||
events.push(Event::KeyRelease(key_release));
|
||||
}
|
||||
}
|
||||
events.push(Event::KeyPress(key_press));
|
||||
last_key_press = Some(key_press);
|
||||
}
|
||||
_ => {
|
||||
if let Some(release_event) = last_key_release.take() {
|
||||
events.push(release_event);
|
||||
}
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Add any remaining stored KeyRelease event
|
||||
if let Some(release_event) = last_key_release.take() {
|
||||
events.push(release_event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("error polling for X11 events: {e:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if events.is_empty() && windows_to_refresh.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for window in windows_to_refresh.into_iter() {
|
||||
if let Some(window) = self.get_window(window) {
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
for event in events.into_iter() {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let xim_callback_event = xim_handler.last_callback_event.take();
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
|
||||
if let Some(event) = xim_callback_event {
|
||||
self.handle_xim_callback_event(event);
|
||||
}
|
||||
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
|
||||
if xim_connected {
|
||||
self.xim_handle_event(event);
|
||||
} else {
|
||||
self.handle_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable_ime(&self) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.ximc.is_none() {
|
||||
@@ -375,7 +509,7 @@ impl X11Client {
|
||||
.push(AttributeName::ClientWindow, xim_handler.window)
|
||||
.push(AttributeName::FocusWindow, xim_handler.window);
|
||||
|
||||
let window_id = state.focused_window;
|
||||
let window_id = state.keyboard_focused_window;
|
||||
drop(state);
|
||||
if let Some(window_id) = window_id {
|
||||
let window = self.get_window(window_id).unwrap();
|
||||
@@ -418,110 +552,6 @@ impl X11Client {
|
||||
.map(|window_reference| window_reference.window.clone())
|
||||
}
|
||||
|
||||
fn read_x11_events(&self) -> (HashSet<u32>, Vec<Event>) {
|
||||
let mut events = Vec::new();
|
||||
let mut windows_to_refresh = HashSet::new();
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let mut last_key_release: Option<Event> = None;
|
||||
|
||||
loop {
|
||||
match state.xcb_connection.poll_for_event() {
|
||||
Ok(Some(event)) => {
|
||||
if let Event::Expose(expose_event) = event {
|
||||
windows_to_refresh.insert(expose_event.window);
|
||||
} else {
|
||||
match event {
|
||||
Event::KeyRelease(_) => {
|
||||
last_key_release = Some(event);
|
||||
}
|
||||
Event::KeyPress(key_press) => {
|
||||
if let Some(Event::KeyRelease(key_release)) =
|
||||
last_key_release.take()
|
||||
{
|
||||
// We ignore that last KeyRelease if it's too close to this KeyPress,
|
||||
// suggesting that it's auto-generated by X11 as a key-repeat event.
|
||||
if key_release.detail != key_press.detail
|
||||
|| key_press.time.wrapping_sub(key_release.time) > 20
|
||||
{
|
||||
events.push(Event::KeyRelease(key_release));
|
||||
}
|
||||
}
|
||||
events.push(Event::KeyPress(key_press));
|
||||
}
|
||||
_ => {
|
||||
if let Some(release_event) = last_key_release.take() {
|
||||
events.push(release_event);
|
||||
}
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Add any remaining stored KeyRelease event
|
||||
if let Some(release_event) = last_key_release.take() {
|
||||
events.push(release_event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("error polling for X11 events: {e:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(windows_to_refresh, events)
|
||||
}
|
||||
|
||||
fn process_x11_events(&self, events: Vec<Event>) {
|
||||
for event in events.into_iter() {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
|
||||
// let xim_filtered = false;
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let xim_callback_event = xim_handler.last_callback_event.take();
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
|
||||
if let Some(event) = xim_callback_event {
|
||||
drop(state);
|
||||
self.handle_xim_callback_event(event);
|
||||
} else {
|
||||
drop(state);
|
||||
}
|
||||
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
|
||||
if xim_connected {
|
||||
self.xim_handle_event(event);
|
||||
} else {
|
||||
self.handle_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&self, event: Event) -> Option<()> {
|
||||
match event {
|
||||
Event::ClientMessage(event) => {
|
||||
@@ -561,23 +591,19 @@ impl X11Client {
|
||||
let window = self.get_window(event.window)?;
|
||||
window.property_notify(event);
|
||||
}
|
||||
Event::Expose(event) => {
|
||||
let window = self.get_window(event.window)?;
|
||||
window.refresh();
|
||||
}
|
||||
Event::FocusIn(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
window.set_focused(true);
|
||||
window.set_active(true);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.focused_window = Some(event.event);
|
||||
state.keyboard_focused_window = Some(event.event);
|
||||
drop(state);
|
||||
self.enable_ime();
|
||||
}
|
||||
Event::FocusOut(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
window.set_focused(false);
|
||||
window.set_active(false);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.focused_window = None;
|
||||
state.keyboard_focused_window = None;
|
||||
if let Some(compose_state) = state.compose_state.as_mut() {
|
||||
compose_state.reset();
|
||||
}
|
||||
@@ -592,8 +618,8 @@ impl X11Client {
|
||||
event.base_mods.into(),
|
||||
event.latched_mods.into(),
|
||||
event.locked_mods.into(),
|
||||
0,
|
||||
0,
|
||||
event.base_group as u32,
|
||||
event.latched_group as u32,
|
||||
event.locked_group.into(),
|
||||
);
|
||||
|
||||
@@ -601,7 +627,7 @@ impl X11Client {
|
||||
if state.modifiers == modifiers {
|
||||
drop(state);
|
||||
} else {
|
||||
let focused_window_id = state.focused_window?;
|
||||
let focused_window_id = state.keyboard_focused_window?;
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
@@ -617,6 +643,7 @@ impl X11Client {
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
state.modifiers = modifiers;
|
||||
state.pre_ime_key_down.take();
|
||||
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
@@ -831,10 +858,15 @@ impl X11Client {
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = old_scroll - new_scroll;
|
||||
let (x, y) = if !modifiers.shift {
|
||||
(0.0, delta_scroll)
|
||||
} else {
|
||||
(delta_scroll, 0.0)
|
||||
};
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(0.0, delta_scroll)),
|
||||
delta: ScrollDelta::Lines(Point::new(x, y)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
},
|
||||
@@ -846,12 +878,18 @@ impl X11Client {
|
||||
valuator_idx += 1;
|
||||
}
|
||||
}
|
||||
Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
let window = self.get_window(event.event)?;
|
||||
window.set_hovered(true);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.mouse_focused_window = Some(event.event);
|
||||
}
|
||||
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
self.0.borrow_mut().scroll_y = None;
|
||||
|
||||
let window = self.get_window(event.event)?;
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.mouse_focused_window = None;
|
||||
let pressed_button = pressed_button_from_mask(event.buttons[0]);
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
@@ -861,11 +899,13 @@ impl X11Client {
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
let window = self.get_window(event.event)?;
|
||||
window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
|
||||
pressed_button,
|
||||
position,
|
||||
modifiers,
|
||||
}));
|
||||
window.set_hovered(false);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@@ -891,6 +931,11 @@ impl X11Client {
|
||||
match event {
|
||||
Event::KeyPress(event) | Event::KeyRelease(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.pre_ime_key_down = Some(Keystroke::from_xkb(
|
||||
&state.xkb,
|
||||
state.modifiers,
|
||||
event.detail.into(),
|
||||
));
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
drop(state);
|
||||
@@ -917,6 +962,16 @@ impl X11Client {
|
||||
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
|
||||
let window = self.get_window(window).unwrap();
|
||||
let mut state = self.0.borrow_mut();
|
||||
if !state.composing {
|
||||
if let Some(keystroke) = state.pre_ime_key_down.take() {
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
}));
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
state.composing = false;
|
||||
drop(state);
|
||||
|
||||
@@ -966,13 +1021,11 @@ impl X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
const XCB_CONNECTION_TOKEN: Token = Token(0);
|
||||
const WAKER_TOKEN: Token = Token(1);
|
||||
|
||||
impl LinuxClient for X11Client {
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
"X11"
|
||||
}
|
||||
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
|
||||
f(&mut self.0.borrow_mut().common)
|
||||
}
|
||||
@@ -1039,8 +1092,61 @@ impl LinuxClient for X11Client {
|
||||
state.common.appearance,
|
||||
)?;
|
||||
|
||||
let screen_resources = state
|
||||
.xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = state
|
||||
.xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
|
||||
screen_resources
|
||||
.modes
|
||||
.iter()
|
||||
.find(|m| m.id == crtc_info.mode)
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
let refresh_event_token = state
|
||||
.loop_handle
|
||||
.insert_source(calloop::timer::Timer::immediate(), {
|
||||
let refresh_duration = mode_refresh_rate(mode);
|
||||
move |mut instant, (), client| {
|
||||
let xcb_connection = {
|
||||
let state = client.0.borrow_mut();
|
||||
let xcb_connection = state.xcb_connection.clone();
|
||||
if let Some(window) = state.windows.get(&x_window) {
|
||||
let window = window.window.clone();
|
||||
drop(state);
|
||||
window.refresh();
|
||||
}
|
||||
xcb_connection
|
||||
};
|
||||
client.process_x11_events(&xcb_connection).log_err();
|
||||
|
||||
// Take into account that some frames have been skipped
|
||||
let now = Instant::now();
|
||||
while instant < now {
|
||||
instant += refresh_duration;
|
||||
}
|
||||
calloop::timer::TimeoutAction::ToInstant(instant)
|
||||
}
|
||||
})
|
||||
.expect("Failed to initialize refresh timer");
|
||||
|
||||
let window_ref = WindowRef {
|
||||
window: window.0.clone(),
|
||||
refresh_event_token,
|
||||
};
|
||||
|
||||
state.windows.insert(x_window, window_ref);
|
||||
@@ -1049,7 +1155,7 @@ impl LinuxClient for X11Client {
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let Some(focused_window) = state.focused_window else {
|
||||
let Some(focused_window) = state.mouse_focused_window else {
|
||||
return;
|
||||
};
|
||||
let current_style = state
|
||||
@@ -1082,11 +1188,17 @@ impl LinuxClient for X11Client {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("failed to change window cursor");
|
||||
.expect("failed to change window cursor")
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn open_uri(&self, uri: &str) {
|
||||
open_uri_internal(uri, None);
|
||||
open_uri_internal(self.background_executor(), uri, None);
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: PathBuf) {
|
||||
reveal_path_internal(self.background_executor(), path, None);
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {
|
||||
@@ -1163,128 +1275,19 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
|
||||
fn run(&self) {
|
||||
let mut poll = self
|
||||
let mut event_loop = self
|
||||
.0
|
||||
.borrow_mut()
|
||||
.poll
|
||||
.event_loop
|
||||
.take()
|
||||
.context("no poll set on X11Client. calling run more than once is not possible")
|
||||
.unwrap();
|
||||
.expect("App is already running");
|
||||
|
||||
let xcb_fd = self.0.borrow().xcb_connection.as_raw_fd();
|
||||
let mut xcb_source = mio::unix::SourceFd(&xcb_fd);
|
||||
poll.registry()
|
||||
.register(&mut xcb_source, XCB_CONNECTION_TOKEN, Interest::READABLE)
|
||||
.unwrap();
|
||||
|
||||
let mut events = mio::Events::with_capacity(1024);
|
||||
let mut next_refresh_needed = Instant::now();
|
||||
|
||||
'run_loop: loop {
|
||||
let poll_timeout = next_refresh_needed - Instant::now();
|
||||
// We rounding the poll_timeout down so `mio` doesn't round it up to the next higher milliseconds
|
||||
let poll_timeout = Duration::from_millis(poll_timeout.as_millis() as u64);
|
||||
|
||||
if poll_timeout >= Duration::from_millis(1) {
|
||||
let _ = poll.poll(&mut events, Some(poll_timeout));
|
||||
};
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
// Check if we need to quit
|
||||
if let Ok(Some(())) = state.quit_signal_rx.try_recv() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Redraw windows
|
||||
let now = Instant::now();
|
||||
if now > next_refresh_needed {
|
||||
// This will be pulled down to 16ms (or less) if a window is open
|
||||
let mut frame_length = Duration::from_millis(100);
|
||||
|
||||
let mut windows = vec![];
|
||||
for (_, window_ref) in state.windows.iter() {
|
||||
if !window_ref.window.state.borrow().destroyed {
|
||||
frame_length = frame_length.min(window_ref.window.refresh_rate());
|
||||
windows.push(window_ref.window.clone());
|
||||
}
|
||||
}
|
||||
|
||||
drop(state);
|
||||
|
||||
for window in windows {
|
||||
window.refresh();
|
||||
}
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
|
||||
// In the case that we're looping a bit too fast, slow down
|
||||
next_refresh_needed = now.max(next_refresh_needed) + frame_length;
|
||||
}
|
||||
|
||||
// X11 events
|
||||
drop(state);
|
||||
|
||||
loop {
|
||||
let (x_windows, events) = self.read_x11_events();
|
||||
for x_window in x_windows {
|
||||
if let Some(window) = self.get_window(x_window) {
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
if events.len() == 0 {
|
||||
break;
|
||||
}
|
||||
self.process_x11_events(events);
|
||||
|
||||
// When X11 is sending us events faster than we can handle we'll
|
||||
// let the frame rate drop to 10fps to try and avoid getting too behind.
|
||||
if Instant::now() > next_refresh_needed + Duration::from_millis(80) {
|
||||
continue 'run_loop;
|
||||
}
|
||||
}
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
|
||||
// Runnables
|
||||
while let Ok(runnable) = state.runnables.try_recv() {
|
||||
drop(state);
|
||||
runnable.run();
|
||||
state = self.0.borrow_mut();
|
||||
|
||||
if Instant::now() + Duration::from_millis(1) >= next_refresh_needed {
|
||||
continue 'run_loop;
|
||||
}
|
||||
}
|
||||
|
||||
// XDG events
|
||||
if let Ok(event) = state.xdp_event_source.try_recv() {
|
||||
match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
let mut windows = state
|
||||
.windows
|
||||
.values()
|
||||
.map(|window| window.window.clone())
|
||||
.collect::<Vec<_>>();
|
||||
drop(state);
|
||||
|
||||
self.with_common(|common| common.appearance = appearance);
|
||||
for mut window in windows {
|
||||
window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
|
||||
// noop, X11 manages this for us.
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
let state = self.0.borrow();
|
||||
state.focused_window.and_then(|focused_window| {
|
||||
state.keyboard_focused_window.and_then(|focused_window| {
|
||||
state
|
||||
.windows
|
||||
.get(&focused_window)
|
||||
@@ -1293,6 +1296,19 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Adatpted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
|
||||
return Duration::from_millis(16);
|
||||
}
|
||||
|
||||
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
|
||||
let micros = 1_000_000_000 / millihertz;
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
|
||||
value.integral as f32 + value.frac as f32 / u32::MAX as f32
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use util::{maybe, ResultExt};
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
protocol::{
|
||||
randr::{self, ConnectionExt as _},
|
||||
sync,
|
||||
xinput::{self, ConnectionExt as _},
|
||||
xproto::{self, ClientMessageEvent, ConnectionExt, EventMask, TranslateCoordinatesReply},
|
||||
@@ -26,7 +25,7 @@ use x11rb::{
|
||||
|
||||
use std::{
|
||||
cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc,
|
||||
sync::Arc, time::Duration,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use super::{X11Display, XINPUT_MASTER_DEVICE};
|
||||
@@ -55,6 +54,7 @@ x11rb::atom_manager! {
|
||||
_MOTIF_WM_HINTS,
|
||||
_GTK_SHOW_WINDOW_MENU,
|
||||
_GTK_FRAME_EXTENTS,
|
||||
_GTK_EDGE_CONSTRAINTS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,49 @@ impl ResizeEdge {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EdgeConstraints {
|
||||
top_tiled: bool,
|
||||
#[allow(dead_code)]
|
||||
top_resizable: bool,
|
||||
|
||||
right_tiled: bool,
|
||||
#[allow(dead_code)]
|
||||
right_resizable: bool,
|
||||
|
||||
bottom_tiled: bool,
|
||||
#[allow(dead_code)]
|
||||
bottom_resizable: bool,
|
||||
|
||||
left_tiled: bool,
|
||||
#[allow(dead_code)]
|
||||
left_resizable: bool,
|
||||
}
|
||||
|
||||
impl EdgeConstraints {
|
||||
fn from_atom(atom: u32) -> Self {
|
||||
EdgeConstraints {
|
||||
top_tiled: (atom & (1 << 0)) != 0,
|
||||
top_resizable: (atom & (1 << 1)) != 0,
|
||||
right_tiled: (atom & (1 << 2)) != 0,
|
||||
right_resizable: (atom & (1 << 3)) != 0,
|
||||
bottom_tiled: (atom & (1 << 4)) != 0,
|
||||
bottom_resizable: (atom & (1 << 5)) != 0,
|
||||
left_tiled: (atom & (1 << 6)) != 0,
|
||||
left_resizable: (atom & (1 << 7)) != 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tiling(&self) -> Tiling {
|
||||
Tiling {
|
||||
top: self.top_tiled,
|
||||
right: self.right_tiled,
|
||||
bottom: self.bottom_tiled,
|
||||
left: self.left_tiled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Visual {
|
||||
id: xproto::Visualid,
|
||||
@@ -168,6 +211,7 @@ pub struct Callbacks {
|
||||
request_frame: Option<Box<dyn FnMut()>>,
|
||||
input: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
|
||||
active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
hovered_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
moved: Option<Box<dyn FnMut()>>,
|
||||
should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||
@@ -177,7 +221,6 @@ pub struct Callbacks {
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub destroyed: bool,
|
||||
refresh_rate: Duration,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
@@ -196,17 +239,18 @@ pub struct X11WindowState {
|
||||
maximized_horizontal: bool,
|
||||
hidden: bool,
|
||||
active: bool,
|
||||
hovered: bool,
|
||||
fullscreen: bool,
|
||||
client_side_decorations_supported: bool,
|
||||
decorations: WindowDecorations,
|
||||
edge_constraints: Option<EdgeConstraints>,
|
||||
pub handle: AnyWindowHandle,
|
||||
last_insets: [u32; 4],
|
||||
}
|
||||
|
||||
impl X11WindowState {
|
||||
fn is_transparent(&self) -> bool {
|
||||
self.decorations == WindowDecorations::Client
|
||||
|| self.background_appearance != WindowBackgroundAppearance::Opaque
|
||||
self.background_appearance != WindowBackgroundAppearance::Opaque
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +259,7 @@ pub(crate) struct X11WindowStatePtr {
|
||||
pub state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
pub x_window: xproto::Window,
|
||||
x_window: xproto::Window,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
@@ -409,6 +453,7 @@ impl X11WindowState {
|
||||
xinput::XIEventMask::MOTION
|
||||
| xinput::XIEventMask::BUTTON_PRESS
|
||||
| xinput::XIEventMask::BUTTON_RELEASE
|
||||
| xinput::XIEventMask::ENTER
|
||||
| xinput::XIEventMask::LEAVE,
|
||||
],
|
||||
}],
|
||||
@@ -443,36 +488,14 @@ impl X11WindowState {
|
||||
// Note: this has to be done after the GPU init, or otherwise
|
||||
// the sizes are immediately invalidated.
|
||||
size: query_render_extent(xcb_connection, x_window),
|
||||
// In case we have window decorations to render
|
||||
transparent: true,
|
||||
// We set it to transparent by default, even if we have client-side
|
||||
// decorations, since those seem to work on X11 even without `true` here.
|
||||
// If the window appearance changes, then the renderer will get updated
|
||||
// too
|
||||
transparent: false,
|
||||
};
|
||||
xcb_connection.map_window(x_window).unwrap();
|
||||
|
||||
let screen_resources = xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
|
||||
screen_resources
|
||||
.modes
|
||||
.iter()
|
||||
.find(|m| m.id == crtc_info.mode)
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
let refresh_rate = mode_refresh_rate(&mode);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
executor,
|
||||
@@ -487,6 +510,7 @@ impl X11WindowState {
|
||||
atoms: *atoms,
|
||||
input_handler: None,
|
||||
active: false,
|
||||
hovered: false,
|
||||
fullscreen: false,
|
||||
maximized_vertical: false,
|
||||
maximized_horizontal: false,
|
||||
@@ -498,9 +522,9 @@ impl X11WindowState {
|
||||
client_side_decorations_supported,
|
||||
decorations: WindowDecorations::Server,
|
||||
last_insets: [0, 0, 0, 0],
|
||||
edge_constraints: None,
|
||||
counter_id: sync_request_counter,
|
||||
last_sync_counter: None,
|
||||
refresh_rate,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -612,6 +636,8 @@ impl X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -689,6 +715,30 @@ impl X11WindowStatePtr {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if event.atom == state.atoms._NET_WM_STATE {
|
||||
self.set_wm_properties(state);
|
||||
} else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS {
|
||||
self.set_edge_constraints(state);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_edge_constraints(&self, mut state: std::cell::RefMut<X11WindowState>) {
|
||||
let reply = self
|
||||
.xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
self.x_window,
|
||||
state.atoms._GTK_EDGE_CONSTRAINTS,
|
||||
xproto::AtomEnum::CARDINAL,
|
||||
0,
|
||||
4,
|
||||
)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
|
||||
if reply.value_len != 0 {
|
||||
let atom = u32::from_ne_bytes(reply.value[0..4].try_into().unwrap());
|
||||
let edge_constraints = EdgeConstraints::from_atom(atom);
|
||||
state.edge_constraints.replace(edge_constraints);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,6 +781,15 @@ impl X11WindowStatePtr {
|
||||
state.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
let hovered_window = self
|
||||
.xcb_connection
|
||||
.query_pointer(state.x_root_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap()
|
||||
.child;
|
||||
self.set_hovered(hovered_window == self.x_window);
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
@@ -866,12 +925,18 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_focused(&self, focus: bool) {
|
||||
pub fn set_active(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_hovered(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().hovered_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.appearance = appearance;
|
||||
@@ -884,10 +949,6 @@ impl X11WindowStatePtr {
|
||||
(fun)()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_rate(&self) -> Duration {
|
||||
self.state.borrow().refresh_rate
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
@@ -997,12 +1058,17 @@ impl PlatformWindow for X11Window {
|
||||
xproto::Time::CURRENT_TIME,
|
||||
)
|
||||
.log_err();
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.0.state.borrow().active
|
||||
}
|
||||
|
||||
fn is_hovered(&self) -> bool {
|
||||
self.0.state.borrow().hovered
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.0
|
||||
.xcb_connection
|
||||
@@ -1025,6 +1091,7 @@ impl PlatformWindow for X11Window {
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
@@ -1042,6 +1109,8 @@ impl PlatformWindow for X11Window {
|
||||
xproto::AtomEnum::STRING,
|
||||
&data,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1077,6 +1146,8 @@ impl PlatformWindow for X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1114,6 +1185,10 @@ impl PlatformWindow for X11Window {
|
||||
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.callbacks.borrow_mut().hovered_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
@@ -1167,6 +1242,8 @@ impl PlatformWindow for X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1190,15 +1267,20 @@ impl PlatformWindow for X11Window {
|
||||
match state.decorations {
|
||||
WindowDecorations::Server => Decorations::Server,
|
||||
WindowDecorations::Client => {
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d
|
||||
Decorations::Client {
|
||||
tiling: Tiling {
|
||||
let tiling = if state.fullscreen {
|
||||
Tiling::tiled()
|
||||
} else if let Some(edge_constraints) = &state.edge_constraints {
|
||||
edge_constraints.to_tiling()
|
||||
} else {
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d
|
||||
Tiling {
|
||||
top: state.maximized_vertical,
|
||||
bottom: state.maximized_vertical,
|
||||
left: state.maximized_horizontal,
|
||||
right: state.maximized_horizontal,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
Decorations::Client { tiling }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1208,17 +1290,28 @@ impl PlatformWindow for X11Window {
|
||||
|
||||
let dp = (inset.0 * state.scale_factor) as u32;
|
||||
|
||||
let (left, right) = if state.maximized_horizontal {
|
||||
(0, 0)
|
||||
let insets = if state.fullscreen {
|
||||
[0, 0, 0, 0]
|
||||
} else if let Some(edge_constraints) = &state.edge_constraints {
|
||||
let left = if edge_constraints.left_tiled { 0 } else { dp };
|
||||
let top = if edge_constraints.top_tiled { 0 } else { dp };
|
||||
let right = if edge_constraints.right_tiled { 0 } else { dp };
|
||||
let bottom = if edge_constraints.bottom_tiled { 0 } else { dp };
|
||||
|
||||
[left, right, top, bottom]
|
||||
} else {
|
||||
(dp, dp)
|
||||
let (left, right) = if state.maximized_horizontal {
|
||||
(0, 0)
|
||||
} else {
|
||||
(dp, dp)
|
||||
};
|
||||
let (top, bottom) = if state.maximized_vertical {
|
||||
(0, 0)
|
||||
} else {
|
||||
(dp, dp)
|
||||
};
|
||||
[left, right, top, bottom]
|
||||
};
|
||||
let (top, bottom) = if state.maximized_vertical {
|
||||
(0, 0)
|
||||
} else {
|
||||
(dp, dp)
|
||||
};
|
||||
let insets = [left, right, top, bottom];
|
||||
|
||||
if state.last_insets != insets {
|
||||
state.last_insets = insets;
|
||||
@@ -1234,6 +1327,8 @@ impl PlatformWindow for X11Window {
|
||||
4,
|
||||
bytemuck::cast_slice::<u32, u8>(&insets),
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1267,6 +1362,8 @@ impl PlatformWindow for X11Window {
|
||||
5,
|
||||
bytemuck::cast_slice::<u32, u8>(&hints_data),
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
|
||||
match decorations {
|
||||
@@ -1289,16 +1386,3 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
|
||||
return Duration::from_millis(16);
|
||||
}
|
||||
|
||||
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
|
||||
let micros = 1_000_000_000 / millihertz;
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user