Compare commits
116 Commits
linux-acti
...
linux/trac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ac6a2f5c2 | ||
|
|
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 | ||
|
|
c8b106245c | ||
|
|
398c2f91dd | ||
|
|
3a5d116ffe | ||
|
|
e3cd1dd2d0 | ||
|
|
b1f8fc88a1 | ||
|
|
d450a1d9e6 | ||
|
|
818e6e53d6 |
@@ -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
.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": {
|
||||
|
||||
91
Cargo.lock
generated
91
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",
|
||||
@@ -374,6 +373,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"async-watch",
|
||||
"breadcrumbs",
|
||||
"cargo_toml",
|
||||
"chrono",
|
||||
"client",
|
||||
@@ -383,7 +383,6 @@ dependencies = [
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feature_flags",
|
||||
"file_icons",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
@@ -3933,6 +3932,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"snippet_provider",
|
||||
"task",
|
||||
"theme",
|
||||
"toml 0.8.10",
|
||||
@@ -4889,11 +4889,9 @@ dependencies = [
|
||||
"log",
|
||||
"media",
|
||||
"metal",
|
||||
"mio 1.0.0",
|
||||
"num_cpus",
|
||||
"objc",
|
||||
"oo7",
|
||||
"open",
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
@@ -5691,7 +5689,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"tempfile",
|
||||
@@ -5705,25 +5703,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 +6619,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 +6857,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
@@ -7225,17 +7191,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 +7356,7 @@ dependencies = [
|
||||
"db",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
"language",
|
||||
@@ -8086,6 +8042,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smol",
|
||||
"snippet",
|
||||
"snippet_provider",
|
||||
"task",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
@@ -8539,7 +8496,7 @@ dependencies = [
|
||||
"task",
|
||||
"terminal_view",
|
||||
"ui",
|
||||
"ui_text_field",
|
||||
"ui_input",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
@@ -9902,6 +9859,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 +10255,7 @@ dependencies = [
|
||||
"story",
|
||||
"strum",
|
||||
"theme",
|
||||
"title_bar",
|
||||
"ui",
|
||||
]
|
||||
|
||||
@@ -11112,7 +11086,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"bytes 1.5.0",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
@@ -11737,7 +11711,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ui_text_field"
|
||||
name = "ui_input"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
@@ -13682,6 +13656,7 @@ dependencies = [
|
||||
"settings",
|
||||
"simplelog",
|
||||
"smol",
|
||||
"snippet_provider",
|
||||
"supermaven",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
@@ -13720,9 +13695,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 +13809,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",
|
||||
]
|
||||
|
||||
18
Cargo.toml
18
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"
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
"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",
|
||||
@@ -57,92 +43,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 +98,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 +134,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 +147,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 +231,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 +282,7 @@
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == c",
|
||||
"bindings": {
|
||||
"s": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"ChangeSurrounds": {}
|
||||
}
|
||||
]
|
||||
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -492,12 +327,7 @@
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == y",
|
||||
"bindings": {
|
||||
"s": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"AddSurrounds": {}
|
||||
}
|
||||
]
|
||||
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -522,12 +352,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 +387,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 +408,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 +430,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 +504,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": [
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
async-watch.workspace = true
|
||||
breadcrumbs.workspace = true
|
||||
cargo_toml.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
@@ -24,7 +25,6 @@ collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
|
||||
@@ -49,7 +49,8 @@ actions!(
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
InsertActivePrompt,
|
||||
ToggleHistory,
|
||||
DeployHistory,
|
||||
DeployPromptLibrary,
|
||||
ApplyEdit,
|
||||
ConfirmCommand,
|
||||
ToggleModelSelector
|
||||
@@ -162,7 +163,7 @@ impl LanguageModelRequestMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct LanguageModelRequest {
|
||||
pub model: LanguageModel,
|
||||
pub messages: Vec<LanguageModelRequestMessage>,
|
||||
|
||||
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,
|
||||
|
||||
@@ -11,6 +11,8 @@ pub use cloud::*;
|
||||
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,107 @@
|
||||
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()
|
||||
#[cfg(test)]
|
||||
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);
|
||||
|
||||
@@ -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,18 +68,15 @@ 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)
|
||||
}),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomRight)
|
||||
.attach(gpui::AnchorCorner::BottomLeft)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,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 +178,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 +194,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>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
@@ -5152,7 +5159,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 +10332,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));
|
||||
@@ -11752,6 +11758,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 +11858,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 +12823,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
|
||||
|
||||
@@ -3660,6 +3660,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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use ui::{prelude::*, window_is_transparent, Tooltip};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -587,15 +587,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 +604,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{ops::Range, time::Duration};
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::*;
|
||||
use unicode_segmentation::*;
|
||||
@@ -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
|
||||
@@ -162,6 +193,16 @@ impl TextInput {
|
||||
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||
.unwrap_or(self.content.len())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.content = "".into();
|
||||
self.selected_range = 0..0;
|
||||
self.selection_reversed = false;
|
||||
self.marked_range = None;
|
||||
self.last_layout = None;
|
||||
self.last_bounds = None;
|
||||
self.is_selecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewInputHandler for TextInput {
|
||||
@@ -284,6 +325,7 @@ impl Element for TextElement {
|
||||
None
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
@@ -295,6 +337,7 @@ impl Element for TextElement {
|
||||
(cx.request_layout(style, []), ())
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
@@ -385,6 +428,7 @@ impl Element for TextElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
@@ -399,26 +443,31 @@ impl Element for TextElement {
|
||||
ElementInputHandler::new(bounds, self.input.clone()),
|
||||
);
|
||||
if let Some(selection) = prepaint.selection.take() {
|
||||
profiling::scope!("paint_quad selection");
|
||||
cx.paint_quad(selection)
|
||||
}
|
||||
let line = prepaint.line.take().unwrap();
|
||||
line.paint(bounds.origin, cx.line_height(), cx).unwrap();
|
||||
|
||||
if let Some(cursor) = prepaint.cursor.take() {
|
||||
profiling::scope!("paint_quad cursor");
|
||||
cx.paint_quad(cursor);
|
||||
}
|
||||
self.input.update(cx, |input, _cx| {
|
||||
input.last_layout = Some(line);
|
||||
input.last_bounds = Some(bounds);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TextInput {
|
||||
#[profiling::function]
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.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 +478,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 +499,74 @@ 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 InputExample {
|
||||
fn on_reset_click(&mut self, _: &MouseUpEvent, cx: &mut ViewContext<Self>) {
|
||||
self.recent_keystrokes.clear();
|
||||
self.text_input
|
||||
.update(cx, |text_input, _cx| text_input.reset());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InputExample {
|
||||
#[profiling::function]
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let num_keystrokes = self.recent_keystrokes.len();
|
||||
div()
|
||||
.bg(rgb(0xaaaaaa))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.bg(white())
|
||||
.border_b_1()
|
||||
.border_color(black())
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(format!("Keystrokes: {}", num_keystrokes))
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(black())
|
||||
.px_2()
|
||||
.bg(yellow())
|
||||
.child("Reset")
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(yellow().blend(opaque_grey(0.5, 0.5)))
|
||||
.cursor_pointer()
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
|
||||
),
|
||||
)
|
||||
.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,42 +589,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.focus_view(&view.text_input);
|
||||
cx.activate(true);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(5)).await;
|
||||
this.update(&mut cx, |_, cx| {
|
||||
println!("calling cx.activate()");
|
||||
cx.activate(true);
|
||||
println!("cx.activate() called.");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
println!("----------");
|
||||
|
||||
cx.update(|cx| {
|
||||
println!("calling cx.activate_window()");
|
||||
cx.activate_window();
|
||||
println!("cx.activate_window() called");
|
||||
})
|
||||
.unwrap();
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
@@ -387,6 +387,7 @@ impl<E: Element> Drawable<E> {
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub(crate) fn layout_as_root(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
@@ -503,6 +504,7 @@ impl AnyElement {
|
||||
}
|
||||
|
||||
/// Performs layout for this element within the given available space and returns its size.
|
||||
#[profiling::function]
|
||||
pub fn layout_as_root(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
@@ -517,6 +519,7 @@ impl AnyElement {
|
||||
}
|
||||
|
||||
/// Performs layout on this element in the available space, then prepaints it at the given absolute origin.
|
||||
#[profiling::function]
|
||||
pub fn prepaint_as_root(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
@@ -524,7 +527,10 @@ impl AnyElement {
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.layout_as_root(available_space, cx);
|
||||
cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx));
|
||||
cx.with_absolute_element_offset(origin, |cx| {
|
||||
profiling::scope!("with_absolute_element_offset prepaint");
|
||||
self.0.prepaint(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -524,6 +524,7 @@ impl BladeRenderer {
|
||||
self.gpu.destroy_command_encoder(&mut self.command_encoder);
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn draw(&mut self, scene: &Scene) {
|
||||
self.command_encoder.start();
|
||||
self.atlas.before_frame(&mut self.command_encoder);
|
||||
|
||||
@@ -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,28 @@ 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 || {
|
||||
let thread_name = format!("background-{}", i);
|
||||
profiling::register_thread!(&thread_name);
|
||||
for runnable in receiver {
|
||||
let start = Instant::now();
|
||||
|
||||
runnable.run();
|
||||
|
||||
log::trace!(
|
||||
"background thread {}: ran runnable. took: {:?}",
|
||||
i,
|
||||
start.elapsed()
|
||||
);
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -46,6 +57,8 @@ impl LinuxDispatcher {
|
||||
|
||||
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
|
||||
let timer_thread = std::thread::spawn(|| {
|
||||
profiling::register_thread!("timer-thread");
|
||||
|
||||
let mut event_loop: EventLoop<()> =
|
||||
EventLoop::try_new().expect("Failed to initialize timer loop!");
|
||||
|
||||
@@ -79,7 +92,6 @@ impl LinuxDispatcher {
|
||||
Self {
|
||||
parker: Mutex::new(Parker::new()),
|
||||
main_sender,
|
||||
main_waker,
|
||||
timer_sender,
|
||||
background_sender,
|
||||
_background_threads: background_threads,
|
||||
@@ -99,9 +111,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 {
|
||||
@@ -221,10 +210,6 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
|
||||
fn activate(&self, ignoring_other_apps: bool) {
|
||||
println!(
|
||||
"Platform.activate(ignoring_other_apps: {}) called",
|
||||
ignoring_other_apps
|
||||
);
|
||||
log::info!("activate is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
|
||||
@@ -318,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);
|
||||
})
|
||||
@@ -341,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()>) {
|
||||
@@ -508,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 {
|
||||
@@ -632,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(),
|
||||
@@ -669,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 {
|
||||
|
||||
@@ -21,7 +21,6 @@ use wayland_client::protocol::wl_callback::{self, WlCallback};
|
||||
use wayland_client::protocol::wl_data_device_manager::DndAction;
|
||||
use wayland_client::protocol::wl_data_offer::WlDataOffer;
|
||||
use wayland_client::protocol::wl_pointer::AxisSource;
|
||||
use wayland_client::protocol::wl_surface::WlSurface;
|
||||
use wayland_client::protocol::{
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
|
||||
};
|
||||
@@ -62,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::{
|
||||
@@ -73,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,
|
||||
@@ -221,8 +222,7 @@ pub(crate) struct WaylandClientState {
|
||||
data_offers: Vec<DataOffer<WlDataOffer>>,
|
||||
primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
cursor: Cursor,
|
||||
pending_open_uri: Option<String>,
|
||||
pub pending_window_activation: Option<WlSurface>,
|
||||
pending_activation: Option<PendingActivation>,
|
||||
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
|
||||
common: LinuxCommon,
|
||||
}
|
||||
@@ -246,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)]
|
||||
@@ -262,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();
|
||||
@@ -312,7 +326,7 @@ impl WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
if state.windows.is_empty() {
|
||||
state.common.quit_signal.quit();
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,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
|
||||
@@ -445,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() {
|
||||
@@ -532,9 +546,8 @@ impl WaylandClient {
|
||||
data_offers: Vec::new(),
|
||||
primary_data_offer: None,
|
||||
cursor,
|
||||
pending_open_uri: None,
|
||||
pending_activation: None,
|
||||
event_loop: Some(event_loop),
|
||||
pending_window_activation: None,
|
||||
}));
|
||||
|
||||
WaylandSource::new(conn, event_queue)
|
||||
@@ -632,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,7 +844,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
|
||||
match event {
|
||||
wl_callback::Event::Done { .. } => {
|
||||
window.frame(true);
|
||||
window.frame();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -957,22 +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 if let Some(surface) = state.pending_window_activation.take() {
|
||||
println!("we got a surface");
|
||||
if let Some(ref manager) = state.globals.activation {
|
||||
println!("we're calling activate. token: {:?}", token);
|
||||
manager.activate(token, &surface);
|
||||
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))
|
||||
}
|
||||
// if let Some(surface) = state.pending_activations.remove(&token) {
|
||||
// if let Some(ref manager) = state.globals.activation {
|
||||
// manager.activate(token, &surface);
|
||||
// }
|
||||
// }
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1206,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;
|
||||
}
|
||||
|
||||
@@ -1520,6 +1555,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,
|
||||
@@ -1545,6 +1585,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,
|
||||
@@ -1567,6 +1612,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,
|
||||
|
||||
@@ -74,9 +74,9 @@ struct InProgressConfigure {
|
||||
pub struct WaylandWindowState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
acknowledged_first_configure: bool,
|
||||
app_id: Option<String>,
|
||||
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,
|
||||
@@ -101,7 +101,6 @@ pub struct WaylandWindowState {
|
||||
in_progress_window_controls: Option<WindowControls>,
|
||||
window_controls: WindowControls,
|
||||
inset: Option<Pixels>,
|
||||
requested_inset: Option<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -157,10 +156,10 @@ impl WaylandWindowState {
|
||||
|
||||
Ok(Self {
|
||||
xdg_surface,
|
||||
app_id: None,
|
||||
acknowledged_first_configure: false,
|
||||
surface,
|
||||
decoration,
|
||||
app_id: None,
|
||||
blur: None,
|
||||
toplevel,
|
||||
viewport,
|
||||
@@ -191,7 +190,6 @@ impl WaylandWindowState {
|
||||
window_menu: true,
|
||||
},
|
||||
inset: None,
|
||||
requested_inset: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -318,12 +316,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();
|
||||
@@ -353,13 +350,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(),
|
||||
@@ -375,12 +371,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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -472,6 +483,10 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
if fullscreen || maximized {
|
||||
tiling = Tiling::tiled();
|
||||
}
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.in_progress_configure = Some(InProgressConfigure {
|
||||
size,
|
||||
@@ -810,35 +825,19 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
println!("WaylandWindow.activate called");
|
||||
|
||||
// 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();
|
||||
let activation = state.globals.activation.as_ref().cloned();
|
||||
|
||||
if let Some(manager) = activation {
|
||||
let token = manager.get_activation_token(&state.globals.qh, ());
|
||||
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);
|
||||
|
||||
println!("Creating activation token...");
|
||||
token.set_app_id(app_id);
|
||||
token.set_serial(serial, &state.globals.seat);
|
||||
println!("Token serial set: {:?}", serial);
|
||||
token.set_surface(&state.surface);
|
||||
println!("Surface set for activation: {:?}", &state.surface);
|
||||
if let Some(app_id) = state.app_id.as_ref() {
|
||||
token.set_app_id(app_id.clone());
|
||||
println!("App ID set for activation: {}", app_id);
|
||||
}
|
||||
|
||||
state
|
||||
.client
|
||||
.get_client()
|
||||
.borrow_mut()
|
||||
.pending_window_activation = Some(state.surface.clone());
|
||||
|
||||
token.commit();
|
||||
println!("Activation token committed, waiting for done event.");
|
||||
} else {
|
||||
log::warn!("Wayland compositor does not support xdg_activation_v1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,8 +851,8 @@ impl PlatformWindow for WaylandWindow {
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.app_id = Some(app_id.to_owned());
|
||||
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) {
|
||||
@@ -926,26 +925,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();
|
||||
}
|
||||
|
||||
@@ -1006,7 +986,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,19 +95,17 @@ 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,
|
||||
@@ -124,6 +119,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>,
|
||||
@@ -145,46 +141,14 @@ 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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,12 +158,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
|
||||
@@ -241,13 +220,25 @@ impl X11Client {
|
||||
.map(|class| *class)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let atoms = XcbAtoms::new(&xcb_connection).unwrap();
|
||||
let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
|
||||
|
||||
let root = xcb_connection.setup().roots[0].root;
|
||||
let compositor_present = check_compositor_present(&xcb_connection, root);
|
||||
let gtk_frame_extents_supported =
|
||||
check_gtk_frame_extents_supported(&xcb_connection, &atoms, root);
|
||||
let client_side_decorations_supported = compositor_present && gtk_frame_extents_supported;
|
||||
log::info!(
|
||||
"x11: compositor present: {}, gtk_frame_extents_supported: {}",
|
||||
compositor_present,
|
||||
gtk_frame_extents_supported
|
||||
);
|
||||
|
||||
let xkb = xcb_connection
|
||||
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
|
||||
let atoms = atoms.reply().unwrap();
|
||||
let xkb = xkb.reply().unwrap();
|
||||
let events = xkb::EventType::STATE_NOTIFY;
|
||||
xcb_connection
|
||||
.xkb_select_events(
|
||||
@@ -298,24 +289,54 @@ 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,
|
||||
scale_factor,
|
||||
|
||||
xcb_connection,
|
||||
client_side_decorations_supported,
|
||||
x_root_index,
|
||||
_resource_database: resource_database,
|
||||
atoms,
|
||||
@@ -327,6 +348,7 @@ impl X11Client {
|
||||
|
||||
compose_state,
|
||||
pre_edit_text: None,
|
||||
pre_ime_key_down: None,
|
||||
composing: false,
|
||||
|
||||
cursor_handle,
|
||||
@@ -342,6 +364,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() {
|
||||
@@ -404,110 +545,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) => {
|
||||
@@ -547,10 +584,6 @@ 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);
|
||||
@@ -578,8 +611,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(),
|
||||
);
|
||||
|
||||
@@ -603,6 +636,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();
|
||||
@@ -817,10 +851,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(),
|
||||
},
|
||||
@@ -877,6 +916,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);
|
||||
@@ -903,6 +947,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);
|
||||
|
||||
@@ -952,13 +1006,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)
|
||||
}
|
||||
@@ -1017,6 +1069,7 @@ impl LinuxClient for X11Client {
|
||||
state.common.foreground_executor.clone(),
|
||||
params,
|
||||
&state.xcb_connection,
|
||||
state.client_side_decorations_supported,
|
||||
state.x_root_index,
|
||||
x_window,
|
||||
&state.atoms,
|
||||
@@ -1024,8 +1077,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);
|
||||
@@ -1067,11 +1173,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) {
|
||||
@@ -1148,123 +1260,14 @@ 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> {
|
||||
@@ -1278,6 +1281,120 @@ 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
|
||||
}
|
||||
|
||||
fn check_compositor_present(xcb_connection: &XCBConnection, root: u32) -> bool {
|
||||
// Method 1: Check for _NET_WM_CM_S{root}
|
||||
let atom_name = format!("_NET_WM_CM_S{}", root);
|
||||
let atom = xcb_connection
|
||||
.intern_atom(false, atom_name.as_bytes())
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.atom)
|
||||
.unwrap_or(0);
|
||||
|
||||
let method1 = if atom != 0 {
|
||||
xcb_connection
|
||||
.get_selection_owner(atom)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.owner != 0)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Method 2: Check for _NET_WM_CM_OWNER
|
||||
let atom_name = "_NET_WM_CM_OWNER";
|
||||
let atom = xcb_connection
|
||||
.intern_atom(false, atom_name.as_bytes())
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.atom)
|
||||
.unwrap_or(0);
|
||||
|
||||
let method2 = if atom != 0 {
|
||||
xcb_connection
|
||||
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.value_len > 0)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Method 3: Check for _NET_SUPPORTING_WM_CHECK
|
||||
let atom_name = "_NET_SUPPORTING_WM_CHECK";
|
||||
let atom = xcb_connection
|
||||
.intern_atom(false, atom_name.as_bytes())
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.atom)
|
||||
.unwrap_or(0);
|
||||
|
||||
let method3 = if atom != 0 {
|
||||
xcb_connection
|
||||
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.value_len > 0)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// TODO: Remove this
|
||||
log::info!(
|
||||
"Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}",
|
||||
method1,
|
||||
method2,
|
||||
method3
|
||||
);
|
||||
|
||||
method1 || method2 || method3
|
||||
}
|
||||
|
||||
fn check_gtk_frame_extents_supported(
|
||||
xcb_connection: &XCBConnection,
|
||||
atoms: &XcbAtoms,
|
||||
root: xproto::Window,
|
||||
) -> bool {
|
||||
let supported_atoms = xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
root,
|
||||
atoms._NET_SUPPORTED,
|
||||
xproto::AtomEnum::ATOM,
|
||||
0,
|
||||
1024,
|
||||
)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| {
|
||||
// Convert Vec<u8> to Vec<u32>
|
||||
reply
|
||||
.value
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||||
.collect::<Vec<u32>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
supported_atoms.contains(&atoms._GTK_FRAME_EXTENTS)
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
@@ -51,9 +50,11 @@ x11rb::atom_manager! {
|
||||
_NET_WM_WINDOW_TYPE,
|
||||
_NET_WM_WINDOW_TYPE_NOTIFICATION,
|
||||
_NET_WM_SYNC,
|
||||
_NET_SUPPORTED,
|
||||
_MOTIF_WM_HINTS,
|
||||
_GTK_SHOW_WINDOW_MENU,
|
||||
_GTK_FRAME_EXTENTS,
|
||||
_GTK_EDGE_CONSTRAINTS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,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,
|
||||
@@ -176,7 +220,6 @@ pub struct Callbacks {
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub destroyed: bool,
|
||||
refresh_rate: Duration,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
@@ -196,15 +239,16 @@ pub struct X11WindowState {
|
||||
hidden: bool,
|
||||
active: 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +257,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 {
|
||||
@@ -251,6 +295,7 @@ impl X11WindowState {
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
client_side_decorations_supported: bool,
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
@@ -440,36 +485,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,
|
||||
@@ -492,11 +515,12 @@ impl X11WindowState {
|
||||
handle,
|
||||
background_appearance: WindowBackgroundAppearance::Opaque,
|
||||
destroyed: false,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -560,6 +584,7 @@ impl X11Window {
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
client_side_decorations_supported: bool,
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
@@ -573,6 +598,7 @@ impl X11Window {
|
||||
executor,
|
||||
params,
|
||||
xcb_connection,
|
||||
client_side_decorations_supported,
|
||||
x_main_screen_index,
|
||||
x_window,
|
||||
atoms,
|
||||
@@ -606,6 +632,8 @@ impl X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -683,6 +711,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -878,10 +930,6 @@ impl X11WindowStatePtr {
|
||||
(fun)()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_rate(&self) -> Duration {
|
||||
self.state.borrow().refresh_rate
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
@@ -991,6 +1039,7 @@ impl PlatformWindow for X11Window {
|
||||
xproto::Time::CURRENT_TIME,
|
||||
)
|
||||
.log_err();
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
@@ -1019,6 +1068,7 @@ impl PlatformWindow for X11Window {
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
@@ -1036,6 +1086,8 @@ impl PlatformWindow for X11Window {
|
||||
xproto::AtomEnum::STRING,
|
||||
&data,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1071,6 +1123,8 @@ impl PlatformWindow for X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1128,6 +1182,7 @@ impl PlatformWindow for X11Window {
|
||||
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn draw(&self, scene: &Scene) {
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
inner.renderer.draw(scene);
|
||||
@@ -1161,6 +1216,8 @@ impl PlatformWindow for X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1176,18 +1233,28 @@ impl PlatformWindow for X11Window {
|
||||
fn window_decorations(&self) -> crate::Decorations {
|
||||
let state = self.0.state.borrow();
|
||||
|
||||
// Client window decorations require compositor support
|
||||
if !state.client_side_decorations_supported {
|
||||
return Decorations::Server;
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1197,17 +1264,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;
|
||||
@@ -1223,19 +1301,30 @@ impl PlatformWindow for X11Window {
|
||||
4,
|
||||
bytemuck::cast_slice::<u32, u8>(&insets),
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn request_decorations(&self, decorations: crate::WindowDecorations) {
|
||||
fn request_decorations(&self, mut decorations: crate::WindowDecorations) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
|
||||
if matches!(decorations, crate::WindowDecorations::Client)
|
||||
&& !state.client_side_decorations_supported
|
||||
{
|
||||
log::info!(
|
||||
"x11: no compositor present, falling back to server-side window decorations"
|
||||
);
|
||||
decorations = crate::WindowDecorations::Server;
|
||||
}
|
||||
|
||||
// https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87
|
||||
let hints_data: [u32; 5] = match decorations {
|
||||
WindowDecorations::Server => [1 << 1, 0, 1, 0, 0],
|
||||
WindowDecorations::Client => [1 << 1, 0, 0, 0, 0],
|
||||
};
|
||||
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
|
||||
self.0
|
||||
.xcb_connection
|
||||
.change_property(
|
||||
@@ -1247,6 +1336,8 @@ impl PlatformWindow for X11Window {
|
||||
5,
|
||||
bytemuck::cast_slice::<u32, u8>(&hints_data),
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
|
||||
match decorations {
|
||||
@@ -1269,16 +1360,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)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
//!
|
||||
//! This module uses the [ashpd] crate
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ashpd::desktop::settings::{ColorScheme, Settings};
|
||||
use calloop::channel::{Channel, Sender};
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
|
||||
use mio::Waker;
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use crate::{BackgroundExecutor, WindowAppearance};
|
||||
@@ -24,45 +20,31 @@ pub struct XDPEventSource {
|
||||
}
|
||||
|
||||
impl XDPEventSource {
|
||||
pub fn new(executor: &BackgroundExecutor, waker: Option<Arc<Waker>>) -> Self {
|
||||
pub fn new(executor: &BackgroundExecutor) -> Self {
|
||||
let (sender, channel) = calloop::channel::channel();
|
||||
|
||||
let background = executor.clone();
|
||||
|
||||
executor
|
||||
.spawn(async move {
|
||||
fn send_event<T>(
|
||||
sender: &Sender<T>,
|
||||
waker: &Option<Arc<Waker>>,
|
||||
event: T,
|
||||
) -> Result<(), std::sync::mpsc::SendError<T>> {
|
||||
sender.send(event)?;
|
||||
if let Some(waker) = waker {
|
||||
waker.wake().ok();
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let settings = Settings::new().await?;
|
||||
|
||||
if let Ok(initial_appearance) = settings.color_scheme().await {
|
||||
send_event(
|
||||
&sender,
|
||||
&waker,
|
||||
Event::WindowAppearance(WindowAppearance::from_native(initial_appearance)),
|
||||
)?;
|
||||
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
|
||||
initial_appearance,
|
||||
)))?;
|
||||
}
|
||||
if let Ok(initial_theme) = settings
|
||||
.read::<String>("org.gnome.desktop.interface", "cursor-theme")
|
||||
.await
|
||||
{
|
||||
send_event(&sender, &waker, Event::CursorTheme(initial_theme))?;
|
||||
sender.send(Event::CursorTheme(initial_theme))?;
|
||||
}
|
||||
if let Ok(initial_size) = settings
|
||||
.read::<u32>("org.gnome.desktop.interface", "cursor-size")
|
||||
.await
|
||||
{
|
||||
send_event(&sender, &waker, Event::CursorSize(initial_size))?;
|
||||
sender.send(Event::CursorSize(initial_size))?;
|
||||
}
|
||||
|
||||
if let Ok(mut cursor_theme_changed) = settings
|
||||
@@ -73,12 +55,11 @@ impl XDPEventSource {
|
||||
.await
|
||||
{
|
||||
let sender = sender.clone();
|
||||
let waker = waker.clone();
|
||||
background
|
||||
.spawn(async move {
|
||||
while let Some(theme) = cursor_theme_changed.next().await {
|
||||
let theme = theme?;
|
||||
send_event(&sender, &waker, Event::CursorTheme(theme))?;
|
||||
sender.send(Event::CursorTheme(theme))?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
@@ -93,12 +74,11 @@ impl XDPEventSource {
|
||||
.await
|
||||
{
|
||||
let sender = sender.clone();
|
||||
let waker = waker.clone();
|
||||
background
|
||||
.spawn(async move {
|
||||
while let Some(size) = cursor_size_changed.next().await {
|
||||
let size = size?;
|
||||
send_event(&sender, &waker, Event::CursorSize(size))?;
|
||||
sender.send(Event::CursorSize(size))?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
@@ -107,11 +87,9 @@ impl XDPEventSource {
|
||||
|
||||
let mut appearance_changed = settings.receive_color_scheme_changed().await?;
|
||||
while let Some(scheme) = appearance_changed.next().await {
|
||||
send_event(
|
||||
&sender,
|
||||
&waker,
|
||||
Event::WindowAppearance(WindowAppearance::from_native(scheme)),
|
||||
)?;
|
||||
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
|
||||
scheme,
|
||||
)))?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -120,12 +98,6 @@ impl XDPEventSource {
|
||||
|
||||
Self { channel }
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> anyhow::Result<Event> {
|
||||
self.channel
|
||||
.try_recv()
|
||||
.map_err(|error| anyhow!("{}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for XDPEventSource {
|
||||
|
||||
@@ -49,7 +49,8 @@ struct DirectWriteComponent {
|
||||
|
||||
struct GlyphRenderContext {
|
||||
params: IDWriteRenderingParams3,
|
||||
dc_target: ID2D1DeviceContext4,
|
||||
normal_dc_target: ID2D1DeviceContext4,
|
||||
emoji_dc_target: ID2D1DeviceContext4,
|
||||
}
|
||||
|
||||
// All use of the IUnknown methods should be "thread-safe".
|
||||
@@ -127,7 +128,16 @@ impl GlyphRenderContext {
|
||||
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
|
||||
grid_fit_mode,
|
||||
)?;
|
||||
let dc_target = {
|
||||
let normal_dc_target = {
|
||||
let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property(
|
||||
DXGI_FORMAT_A8_UNORM,
|
||||
D2D1_ALPHA_MODE_STRAIGHT,
|
||||
))?;
|
||||
let target = target.cast::<ID2D1DeviceContext4>()?;
|
||||
target.SetTextRenderingParams(¶ms);
|
||||
target
|
||||
};
|
||||
let emoji_dc_target = {
|
||||
let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property(
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D2D1_ALPHA_MODE_PREMULTIPLIED,
|
||||
@@ -137,7 +147,11 @@ impl GlyphRenderContext {
|
||||
target
|
||||
};
|
||||
|
||||
Ok(Self { params, dc_target })
|
||||
Ok(Self {
|
||||
params,
|
||||
normal_dc_target,
|
||||
emoji_dc_target,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,7 +571,11 @@ impl DirectWriteState {
|
||||
}
|
||||
|
||||
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let render_target = &self.components.render_context.dc_target;
|
||||
let render_target = if params.is_emoji {
|
||||
&self.components.render_context.emoji_dc_target
|
||||
} else {
|
||||
&self.components.render_context.normal_dc_target
|
||||
};
|
||||
unsafe {
|
||||
render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS);
|
||||
render_target.SetDpi(96.0 * params.scale_factor, 96.0 * params.scale_factor);
|
||||
|
||||
@@ -659,6 +659,13 @@ fn handle_calc_client_size(
|
||||
requested_client_rect[0].left += frame_x + padding;
|
||||
requested_client_rect[0].bottom -= frame_y + padding;
|
||||
|
||||
if state_ptr.state.borrow().is_maximized() {
|
||||
requested_client_rect[0].top += frame_y + padding;
|
||||
} else {
|
||||
// Magic number that calculates the width of the border
|
||||
requested_client_rect[0].top += frame_y - 3;
|
||||
}
|
||||
|
||||
Some(0)
|
||||
}
|
||||
|
||||
@@ -821,14 +828,14 @@ fn handle_hit_test_msg(
|
||||
|
||||
let dpi = unsafe { GetDpiForWindow(handle) };
|
||||
let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
|
||||
let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
|
||||
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
};
|
||||
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
|
||||
if cursor_point.y > 0 && cursor_point.y < frame_y + padding {
|
||||
if !state_ptr.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y
|
||||
{
|
||||
return Some(HTTOP as _);
|
||||
}
|
||||
|
||||
@@ -1044,6 +1051,8 @@ fn handle_system_settings_changed(state_ptr: Rc<WindowsWindowStatePtr>) -> Optio
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
// mouse wheel
|
||||
lock.system_settings.mouse_wheel_settings.update();
|
||||
// mouse double click
|
||||
lock.click_state.system_update();
|
||||
Some(0)
|
||||
}
|
||||
|
||||
@@ -1259,7 +1268,7 @@ fn is_modifier(virtual_key: VIRTUAL_KEY) -> bool {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn current_modifiers() -> Modifiers {
|
||||
pub(crate) fn current_modifiers() -> Modifiers {
|
||||
Modifiers {
|
||||
control: is_virtual_key_pressed(VK_CONTROL),
|
||||
alt: is_virtual_key_pressed(VK_MENU),
|
||||
|
||||
@@ -27,10 +27,7 @@ use windows::{
|
||||
System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*, Time::*},
|
||||
UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
|
||||
},
|
||||
UI::{
|
||||
Color,
|
||||
ViewManagement::{UIColorType, UISettings},
|
||||
},
|
||||
UI::ViewManagement::UISettings,
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
@@ -678,25 +675,6 @@ fn load_icon() -> Result<HICON> {
|
||||
Ok(HICON(handle.0))
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes
|
||||
#[inline]
|
||||
fn system_appearance() -> Result<WindowAppearance> {
|
||||
let ui_settings = UISettings::new()?;
|
||||
let foreground_color = ui_settings.GetColorValue(UIColorType::Foreground)?;
|
||||
// If the foreground is light, then is_color_light will evaluate to true,
|
||||
// meaning Dark mode is enabled.
|
||||
if is_color_light(&foreground_color) {
|
||||
Ok(WindowAppearance::Dark)
|
||||
} else {
|
||||
Ok(WindowAppearance::Light)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_color_light(color: &Color) -> bool {
|
||||
((5 * color.G as u32) + (2 * color.R as u32) + color.B as u32) > (8 * 128)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn should_auto_hide_scrollbars() -> Result<bool> {
|
||||
let ui_settings = UISettings::new()?;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use ::util::ResultExt;
|
||||
use windows::Win32::{Foundation::*, UI::WindowsAndMessaging::*};
|
||||
use windows::{
|
||||
Win32::{Foundation::*, UI::WindowsAndMessaging::*},
|
||||
UI::{
|
||||
Color,
|
||||
ViewManagement::{UIColorType, UISettings},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -118,3 +124,22 @@ pub(crate) fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point<Pixels>
|
||||
y: px(y / scale_factor),
|
||||
}
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes
|
||||
#[inline]
|
||||
pub(crate) fn system_appearance() -> Result<WindowAppearance> {
|
||||
let ui_settings = UISettings::new()?;
|
||||
let foreground_color = ui_settings.GetColorValue(UIColorType::Foreground)?;
|
||||
// If the foreground is light, then is_color_light will evaluate to true,
|
||||
// meaning Dark mode is enabled.
|
||||
if is_color_light(&foreground_color) {
|
||||
Ok(WindowAppearance::Dark)
|
||||
} else {
|
||||
Ok(WindowAppearance::Light)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_color_light(color: &Color) -> bool {
|
||||
((5 * color.G as u32) + (2 * color.R as u32) + color.B as u32) > (8 * 128)
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ impl WindowsWindow {
|
||||
.titlebar
|
||||
.as_ref()
|
||||
.map(|titlebar| titlebar.appears_transparent)
|
||||
.unwrap_or(false);
|
||||
.unwrap_or(true);
|
||||
let windowname = HSTRING::from(
|
||||
params
|
||||
.titlebar
|
||||
@@ -383,9 +383,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
self.0.state.borrow().scale_factor
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
fn appearance(&self) -> WindowAppearance {
|
||||
WindowAppearance::Dark
|
||||
system_appearance().log_err().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
@@ -405,9 +404,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
logical_point(point.x as f32, point.y as f32, scale_factor)
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
Modifiers::none()
|
||||
current_modifiers()
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
@@ -787,15 +785,25 @@ pub(crate) struct ClickState {
|
||||
button: MouseButton,
|
||||
last_click: Instant,
|
||||
last_position: Point<DevicePixels>,
|
||||
double_click_spatial_tolerance_width: i32,
|
||||
double_click_spatial_tolerance_height: i32,
|
||||
double_click_interval: Duration,
|
||||
pub(crate) current_count: usize,
|
||||
}
|
||||
|
||||
impl ClickState {
|
||||
pub fn new() -> Self {
|
||||
let double_click_spatial_tolerance_width = unsafe { GetSystemMetrics(SM_CXDOUBLECLK) };
|
||||
let double_click_spatial_tolerance_height = unsafe { GetSystemMetrics(SM_CYDOUBLECLK) };
|
||||
let double_click_interval = Duration::from_millis(unsafe { GetDoubleClickTime() } as u64);
|
||||
|
||||
ClickState {
|
||||
button: MouseButton::Left,
|
||||
last_click: Instant::now(),
|
||||
last_position: Point::default(),
|
||||
double_click_spatial_tolerance_width,
|
||||
double_click_spatial_tolerance_height,
|
||||
double_click_interval,
|
||||
current_count: 0,
|
||||
}
|
||||
}
|
||||
@@ -814,13 +822,19 @@ impl ClickState {
|
||||
self.current_count
|
||||
}
|
||||
|
||||
pub fn system_update(&mut self) {
|
||||
self.double_click_spatial_tolerance_width = unsafe { GetSystemMetrics(SM_CXDOUBLECLK) };
|
||||
self.double_click_spatial_tolerance_height = unsafe { GetSystemMetrics(SM_CYDOUBLECLK) };
|
||||
self.double_click_interval = Duration::from_millis(unsafe { GetDoubleClickTime() } as u64);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_double_click(&self, new_position: Point<DevicePixels>) -> bool {
|
||||
let diff = self.last_position - new_position;
|
||||
|
||||
self.last_click.elapsed() < DOUBLE_CLICK_INTERVAL
|
||||
&& diff.x.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE
|
||||
&& diff.y.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE
|
||||
self.last_click.elapsed() < self.double_click_interval
|
||||
&& diff.x.0.abs() <= self.double_click_spatial_tolerance_width
|
||||
&& diff.y.0.abs() <= self.double_click_spatial_tolerance_height
|
||||
}
|
||||
}
|
||||
|
||||
@@ -928,10 +942,6 @@ fn register_drag_drop(state_ptr: Rc<WindowsWindowStatePtr>) {
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
// https://learn.microsoft.com/en-us/windows/win32/controls/ttm-setdelaytime?redirectedfrom=MSDN
|
||||
const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics
|
||||
const DOUBLE_CLICK_SPATIAL_TOLERANCE: i32 = 4;
|
||||
|
||||
mod windows_renderer {
|
||||
use std::{num::NonZeroIsize, sync::Arc};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::TextStyleRefinement;
|
||||
use crate::{
|
||||
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
|
||||
DefiniteLength, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla,
|
||||
JustifyContent, Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
|
||||
self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength,
|
||||
Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length,
|
||||
SharedString, StyleRefinement, WhiteSpace,
|
||||
};
|
||||
use crate::{BoxShadow, TextStyleRefinement};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use taffy::style::{AlignContent, Display, Overflow};
|
||||
pub use gpui_macros::{
|
||||
box_shadow_style_methods, cursor_style_methods, margin_style_methods, overflow_style_methods,
|
||||
padding_style_methods, position_style_methods, visibility_style_methods,
|
||||
};
|
||||
use taffy::style::{AlignContent, Display};
|
||||
|
||||
/// A trait for elements that can be styled.
|
||||
/// Use this to opt-in to a CSS-like styling API.
|
||||
@@ -14,20 +17,13 @@ pub trait Styled: Sized {
|
||||
fn style(&mut self) -> &mut StyleRefinement;
|
||||
|
||||
gpui_macros::style_helpers!();
|
||||
|
||||
/// Sets the position of the element to `relative`.
|
||||
/// [Docs](https://tailwindcss.com/docs/position)
|
||||
fn relative(mut self) -> Self {
|
||||
self.style().position = Some(Position::Relative);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the position of the element to `absolute`.
|
||||
/// [Docs](https://tailwindcss.com/docs/position)
|
||||
fn absolute(mut self) -> Self {
|
||||
self.style().position = Some(Position::Absolute);
|
||||
self
|
||||
}
|
||||
gpui_macros::visibility_style_methods!();
|
||||
gpui_macros::margin_style_methods!();
|
||||
gpui_macros::padding_style_methods!();
|
||||
gpui_macros::position_style_methods!();
|
||||
gpui_macros::overflow_style_methods!();
|
||||
gpui_macros::cursor_style_methods!();
|
||||
gpui_macros::box_shadow_style_methods!();
|
||||
|
||||
/// Sets the display type of the element to `block`.
|
||||
/// [Docs](https://tailwindcss.com/docs/display)
|
||||
@@ -43,195 +39,6 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the visibility of the element to `visible`.
|
||||
/// [Docs](https://tailwindcss.com/docs/visibility)
|
||||
fn visible(mut self) -> Self {
|
||||
self.style().visibility = Some(Visibility::Visible);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the visibility of the element to `hidden`.
|
||||
/// [Docs](https://tailwindcss.com/docs/visibility)
|
||||
fn invisible(mut self) -> Self {
|
||||
self.style().visibility = Some(Visibility::Hidden);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the behavior of content that overflows the container to be hidden.
|
||||
/// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows)
|
||||
fn overflow_hidden(mut self) -> Self {
|
||||
self.style().overflow.x = Some(Overflow::Hidden);
|
||||
self.style().overflow.y = Some(Overflow::Hidden);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the behavior of content that overflows the container on the X axis to be hidden.
|
||||
/// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows)
|
||||
fn overflow_x_hidden(mut self) -> Self {
|
||||
self.style().overflow.x = Some(Overflow::Hidden);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the behavior of content that overflows the container on the Y axis to be hidden.
|
||||
/// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows)
|
||||
fn overflow_y_hidden(mut self) -> Self {
|
||||
self.style().overflow.y = Some(Overflow::Hidden);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the cursor style when hovering over this element
|
||||
fn cursor(mut self, cursor: CursorStyle) -> Self {
|
||||
self.style().mouse_cursor = Some(cursor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the cursor style when hovering an element to `default`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_default(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::Arrow);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the cursor style when hovering an element to `pointer`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_pointer(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::PointingHand);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `text`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_text(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::IBeam);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `move`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_move(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `not-allowed`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_not_allowed(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `context-menu`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_context_menu(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ContextualMenu);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `crosshair`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_crosshair(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::Crosshair);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `vertical-text`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_vertical_text(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::IBeamCursorForVerticalLayout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `alias`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_alias(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::DragLink);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `copy`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_copy(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::DragCopy);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `no-drop`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_no_drop(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `grab`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_grab(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::OpenHand);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `grabbing`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_grabbing(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `ew-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_ew_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ResizeLeftRight);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `ns-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_ns_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ResizeUpDown);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `col-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_col_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ResizeColumn);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `row-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_row_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ResizeRow);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `n-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_n_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ResizeUp);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `e-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_e_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ResizeRight);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `s-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_s_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ResizeDown);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `w-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_w_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::ResizeLeft);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the whitespace of the element to `normal`.
|
||||
/// [Docs](https://tailwindcss.com/docs/whitespace#normal)
|
||||
fn whitespace_normal(mut self) -> Self {
|
||||
@@ -499,104 +306,6 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self {
|
||||
self.style().box_shadow = Some(shadows);
|
||||
self
|
||||
}
|
||||
|
||||
/// Clears the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_none(mut self) -> Self {
|
||||
self.style().box_shadow = Some(Default::default());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_sm(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec::smallvec![BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.05),
|
||||
offset: point(px(0.), px(1.)),
|
||||
blur_radius: px(2.),
|
||||
spread_radius: px(0.),
|
||||
}]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_md(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0.5, 0., 0., 0.1),
|
||||
offset: point(px(0.), px(4.)),
|
||||
blur_radius: px(6.),
|
||||
spread_radius: px(-1.),
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1),
|
||||
offset: point(px(0.), px(2.)),
|
||||
blur_radius: px(4.),
|
||||
spread_radius: px(-2.),
|
||||
}
|
||||
]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_lg(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1),
|
||||
offset: point(px(0.), px(10.)),
|
||||
blur_radius: px(15.),
|
||||
spread_radius: px(-3.),
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1),
|
||||
offset: point(px(0.), px(4.)),
|
||||
blur_radius: px(6.),
|
||||
spread_radius: px(-4.),
|
||||
}
|
||||
]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_xl(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1),
|
||||
offset: point(px(0.), px(20.)),
|
||||
blur_radius: px(25.),
|
||||
spread_radius: px(-5.),
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1),
|
||||
offset: point(px(0.), px(8.)),
|
||||
blur_radius: px(10.),
|
||||
spread_radius: px(-6.),
|
||||
}
|
||||
]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_2xl(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec![BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.25),
|
||||
offset: point(px(0.), px(25.)),
|
||||
blur_radius: px(50.),
|
||||
spread_radius: px(-12.),
|
||||
}]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the text style that has been configured on this element.
|
||||
fn text_style(&mut self) -> &mut Option<TextStyleRefinement> {
|
||||
let style: &mut StyleRefinement = self.style();
|
||||
|
||||
@@ -142,6 +142,7 @@ impl TaffyLayoutEngine {
|
||||
Ok(edges)
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn compute_layout(
|
||||
&mut self,
|
||||
id: LayoutId,
|
||||
@@ -161,6 +162,7 @@ impl TaffyLayoutEngine {
|
||||
//
|
||||
|
||||
if !self.computed_layouts.insert(id) {
|
||||
profiling::scope!("compute layout stack extension");
|
||||
let mut stack = SmallVec::<[LayoutId; 64]>::new();
|
||||
stack.push(id);
|
||||
while let Some(id) = stack.pop() {
|
||||
@@ -181,6 +183,8 @@ impl TaffyLayoutEngine {
|
||||
id.into(),
|
||||
available_space.into(),
|
||||
|known_dimensions, available_space, node_id, _context| {
|
||||
profiling::scope!("measure function");
|
||||
|
||||
let Some(measure) = self.nodes_to_measure.get_mut(&node_id.into()) else {
|
||||
return taffy::geometry::Size::default();
|
||||
};
|
||||
@@ -190,7 +194,10 @@ impl TaffyLayoutEngine {
|
||||
height: known_dimensions.height.map(Pixels),
|
||||
};
|
||||
|
||||
measure(known_dimensions, available_space.into(), cx).into()
|
||||
{
|
||||
profiling::scope!("calling measure");
|
||||
measure(known_dimensions, available_space.into(), cx).into()
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect(EXPECT_MESSAGE);
|
||||
|
||||
@@ -453,17 +453,48 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.element_states.clear();
|
||||
self.accessed_element_states.clear();
|
||||
self.mouse_listeners.clear();
|
||||
self.dispatch_tree.clear();
|
||||
self.scene.clear();
|
||||
self.input_handlers.clear();
|
||||
self.tooltip_requests.clear();
|
||||
self.cursor_styles.clear();
|
||||
self.hitboxes.clear();
|
||||
self.deferred_draws.clear();
|
||||
{
|
||||
profiling::scope!("element_states clear");
|
||||
self.element_states.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("accessed_element_states clear");
|
||||
self.accessed_element_states.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("mouse_listeners clear");
|
||||
self.mouse_listeners.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("dispatch_tree clear");
|
||||
self.dispatch_tree.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("scene clear");
|
||||
self.scene.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("input handlers clear");
|
||||
self.input_handlers.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("tooltip_requests clear");
|
||||
self.tooltip_requests.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("cursor styles clear");
|
||||
self.cursor_styles.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("hitboxes clear");
|
||||
self.hitboxes.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("deferred draws clear");
|
||||
self.deferred_draws.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest {
|
||||
@@ -695,6 +726,7 @@ impl Window {
|
||||
}
|
||||
}));
|
||||
platform_window.on_request_frame(Box::new({
|
||||
profiling::scope!("on_request_frame");
|
||||
let mut cx = cx.to_async();
|
||||
let dirty = dirty.clone();
|
||||
let active = active.clone();
|
||||
@@ -1429,6 +1461,7 @@ impl<'a> WindowContext<'a> {
|
||||
.next_frame
|
||||
.finish(&mut self.window.rendered_frame);
|
||||
ELEMENT_ARENA.with_borrow_mut(|element_arena| {
|
||||
profiling::scope!("element area clear");
|
||||
let percentage = (element_arena.len() as f32 / element_arena.capacity() as f32) * 100.;
|
||||
if percentage >= 80. {
|
||||
log::warn!("elevated element arena occupation: {}.", percentage);
|
||||
@@ -1439,37 +1472,47 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.draw_phase = DrawPhase::Focus;
|
||||
let previous_focus_path = self.window.rendered_frame.focus_path();
|
||||
let previous_window_active = self.window.rendered_frame.window_active;
|
||||
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
|
||||
self.window.next_frame.clear();
|
||||
let current_focus_path = self.window.rendered_frame.focus_path();
|
||||
let current_window_active = self.window.rendered_frame.window_active;
|
||||
|
||||
if previous_focus_path != current_focus_path
|
||||
|| previous_window_active != current_window_active
|
||||
{
|
||||
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
||||
self.window
|
||||
.focus_lost_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(self));
|
||||
}
|
||||
profiling::scope!("swapping frames");
|
||||
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
|
||||
}
|
||||
{
|
||||
profiling::scope!("clearing next frame");
|
||||
self.window.next_frame.clear();
|
||||
}
|
||||
|
||||
let event = WindowFocusEvent {
|
||||
previous_focus_path: if previous_window_active {
|
||||
previous_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
current_focus_path: if current_window_active {
|
||||
current_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
};
|
||||
self.window
|
||||
.focus_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(&event, self));
|
||||
{
|
||||
profiling::scope!("updating focus path");
|
||||
let current_focus_path = self.window.rendered_frame.focus_path();
|
||||
let current_window_active = self.window.rendered_frame.window_active;
|
||||
|
||||
if previous_focus_path != current_focus_path
|
||||
|| previous_window_active != current_window_active
|
||||
{
|
||||
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
||||
self.window
|
||||
.focus_lost_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(self));
|
||||
}
|
||||
|
||||
let event = WindowFocusEvent {
|
||||
previous_focus_path: if previous_window_active {
|
||||
previous_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
current_focus_path: if current_window_active {
|
||||
current_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
};
|
||||
self.window
|
||||
.focus_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(&event, self));
|
||||
}
|
||||
}
|
||||
|
||||
self.reset_cursor_style();
|
||||
@@ -1487,6 +1530,7 @@ impl<'a> WindowContext<'a> {
|
||||
profiling::finish_frame!();
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn draw_roots(&mut self) {
|
||||
self.window.draw_phase = DrawPhase::Prepaint;
|
||||
self.window.tooltip_bounds.take();
|
||||
@@ -1854,6 +1898,7 @@ impl<'a> WindowContext<'a> {
|
||||
/// Updates the global element offset based on the given offset. This is used to implement
|
||||
/// drag handles and other manual painting of elements. This method should only be called during
|
||||
/// the prepaint phase of element drawing.
|
||||
#[profiling::function]
|
||||
pub fn with_absolute_element_offset<R>(
|
||||
&mut self,
|
||||
offset: Point<Pixels>,
|
||||
@@ -2719,6 +2764,7 @@ impl<'a> WindowContext<'a> {
|
||||
/// After calling it, you can request the bounds of the given layout node id or any descendant.
|
||||
///
|
||||
/// This method should only be called as part of the prepaint phase of element drawing.
|
||||
#[profiling::function]
|
||||
pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
|
||||
debug_assert_eq!(
|
||||
self.window.draw_phase,
|
||||
@@ -2727,7 +2773,10 @@ impl<'a> WindowContext<'a> {
|
||||
);
|
||||
|
||||
let mut layout_engine = self.window.layout_engine.take().unwrap();
|
||||
layout_engine.compute_layout(layout_id, available_space, self);
|
||||
{
|
||||
profiling::scope!("layout_engine compute_layout");
|
||||
layout_engine.compute_layout(layout_id, available_space, self);
|
||||
}
|
||||
self.window.layout_engine = Some(layout_engine);
|
||||
}
|
||||
|
||||
@@ -3578,7 +3627,6 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
/// Focus the current window and bring it to the foreground at the platform level.
|
||||
pub fn activate_window(&self) {
|
||||
println!("WindowContext.activate_window called");
|
||||
self.window.platform_window.activate();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod derive_into_element;
|
||||
mod derive_render;
|
||||
mod register_action;
|
||||
mod style_helpers;
|
||||
mod styles;
|
||||
mod test;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
@@ -27,11 +27,53 @@ pub fn derive_render(input: TokenStream) -> TokenStream {
|
||||
derive_render::derive_render(input)
|
||||
}
|
||||
|
||||
/// Used by gpui to generate the style helpers.
|
||||
/// Used by GPUI to generate the style helpers.
|
||||
#[proc_macro]
|
||||
#[doc(hidden)]
|
||||
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
||||
style_helpers::style_helpers(input)
|
||||
styles::style_helpers(input)
|
||||
}
|
||||
|
||||
/// Generates methods for visibility styles.
|
||||
#[proc_macro]
|
||||
pub fn visibility_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::visibility_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for margin styles.
|
||||
#[proc_macro]
|
||||
pub fn margin_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::margin_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for padding styles.
|
||||
#[proc_macro]
|
||||
pub fn padding_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::padding_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for position styles.
|
||||
#[proc_macro]
|
||||
pub fn position_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::position_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for overflow styles.
|
||||
#[proc_macro]
|
||||
pub fn overflow_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::overflow_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for cursor styles.
|
||||
#[proc_macro]
|
||||
pub fn cursor_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::cursor_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for box shadow styles.
|
||||
#[proc_macro]
|
||||
pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::box_shadow_style_methods(input)
|
||||
}
|
||||
|
||||
/// #[gpui::test] can be used to annotate test functions that run with GPUI support.
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream, Result},
|
||||
parse_macro_input,
|
||||
};
|
||||
|
||||
struct StyleableMacroInput;
|
||||
|
||||
impl Parse for StyleableMacroInput {
|
||||
fn parse(_input: ParseStream) -> Result<Self> {
|
||||
Ok(StyleableMacroInput)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
||||
let _ = parse_macro_input!(input as StyleableMacroInput);
|
||||
let methods = generate_methods();
|
||||
let output = quote! {
|
||||
#(#methods)*
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
||||
|
||||
fn generate_methods() -> Vec<TokenStream2> {
|
||||
let mut methods = Vec::new();
|
||||
|
||||
for (prefix, auto_allowed, fields, prefix_doc_string) in box_prefixes() {
|
||||
methods.push(generate_custom_value_setter(
|
||||
prefix,
|
||||
if auto_allowed {
|
||||
quote! { Length }
|
||||
} else {
|
||||
quote! { DefiniteLength }
|
||||
},
|
||||
&fields,
|
||||
prefix_doc_string,
|
||||
));
|
||||
|
||||
for (suffix, length_tokens, suffix_doc_string) in box_suffixes() {
|
||||
if suffix != "auto" || auto_allowed {
|
||||
methods.push(generate_predefined_setter(
|
||||
prefix,
|
||||
suffix,
|
||||
&fields,
|
||||
&length_tokens,
|
||||
false,
|
||||
&format!("{prefix_doc_string}\n\n{suffix_doc_string}"),
|
||||
));
|
||||
}
|
||||
|
||||
if suffix != "auto" {
|
||||
methods.push(generate_predefined_setter(
|
||||
prefix,
|
||||
suffix,
|
||||
&fields,
|
||||
&length_tokens,
|
||||
true,
|
||||
&format!("{prefix_doc_string}\n\n{suffix_doc_string}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (prefix, fields, prefix_doc_string) in corner_prefixes() {
|
||||
methods.push(generate_custom_value_setter(
|
||||
prefix,
|
||||
quote! { AbsoluteLength },
|
||||
&fields,
|
||||
prefix_doc_string,
|
||||
));
|
||||
|
||||
for (suffix, radius_tokens, suffix_doc_string) in corner_suffixes() {
|
||||
methods.push(generate_predefined_setter(
|
||||
prefix,
|
||||
suffix,
|
||||
&fields,
|
||||
&radius_tokens,
|
||||
false,
|
||||
&format!("{prefix_doc_string}\n\n{suffix_doc_string}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for (prefix, fields, prefix_doc_string) in border_prefixes() {
|
||||
methods.push(generate_custom_value_setter(
|
||||
prefix,
|
||||
quote! { AbsoluteLength },
|
||||
&fields,
|
||||
prefix_doc_string,
|
||||
));
|
||||
|
||||
for (suffix, width_tokens, suffix_doc_string) in border_suffixes() {
|
||||
methods.push(generate_predefined_setter(
|
||||
prefix,
|
||||
suffix,
|
||||
&fields,
|
||||
&width_tokens,
|
||||
false,
|
||||
&format!("{prefix_doc_string}\n\n{suffix_doc_string}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
methods
|
||||
}
|
||||
|
||||
fn generate_predefined_setter(
|
||||
name: &'static str,
|
||||
length: &'static str,
|
||||
fields: &[TokenStream2],
|
||||
length_tokens: &TokenStream2,
|
||||
negate: bool,
|
||||
doc_string: &str,
|
||||
) -> TokenStream2 {
|
||||
let (negation_qualifier, negation_token) = if negate {
|
||||
("_neg", quote! { - })
|
||||
} else {
|
||||
("", quote! {})
|
||||
};
|
||||
|
||||
let method_name = if length.is_empty() {
|
||||
format_ident!("{name}{negation_qualifier}")
|
||||
} else {
|
||||
format_ident!("{name}{negation_qualifier}_{length}")
|
||||
};
|
||||
|
||||
let field_assignments = fields
|
||||
.iter()
|
||||
.map(|field_tokens| {
|
||||
quote! {
|
||||
style.#field_tokens = Some((#negation_token gpui::#length_tokens).into());
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let method = quote! {
|
||||
#[doc = #doc_string]
|
||||
fn #method_name(mut self) -> Self {
|
||||
let style = self.style();
|
||||
#(#field_assignments)*
|
||||
self
|
||||
}
|
||||
};
|
||||
|
||||
method
|
||||
}
|
||||
|
||||
fn generate_custom_value_setter(
|
||||
prefix: &str,
|
||||
length_type: TokenStream2,
|
||||
fields: &[TokenStream2],
|
||||
doc_string: &str,
|
||||
) -> TokenStream2 {
|
||||
let method_name = format_ident!("{}", prefix);
|
||||
|
||||
let mut iter = fields.iter();
|
||||
let last = iter.next_back().unwrap();
|
||||
let field_assignments = iter
|
||||
.map(|field_tokens| {
|
||||
quote! {
|
||||
style.#field_tokens = Some(length.clone().into());
|
||||
}
|
||||
})
|
||||
.chain(std::iter::once(quote! {
|
||||
style.#last = Some(length.into());
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let method = quote! {
|
||||
#[doc = #doc_string]
|
||||
fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self {
|
||||
let style = self.style();
|
||||
#(#field_assignments)*
|
||||
self
|
||||
}
|
||||
};
|
||||
|
||||
method
|
||||
}
|
||||
|
||||
/// Returns a vec of (Property name, has 'auto' suffix, tokens for accessing the property, documentation)
|
||||
fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>, &'static str)> {
|
||||
vec![
|
||||
(
|
||||
"w",
|
||||
true,
|
||||
vec![quote! { size.width }],
|
||||
"Sets the width of the element. [Docs](https://tailwindcss.com/docs/width)",
|
||||
),
|
||||
("h", true, vec![quote! { size.height }], "Sets the height of the element. [Docs](https://tailwindcss.com/docs/height)"),
|
||||
(
|
||||
"size",
|
||||
true,
|
||||
vec![quote! {size.width}, quote! {size.height}],
|
||||
"Sets the width and height of the element."
|
||||
),
|
||||
// TODO: These don't use the same size ramp as the others
|
||||
// see https://tailwindcss.com/docs/max-width
|
||||
(
|
||||
"min_w",
|
||||
true,
|
||||
vec![quote! { min_size.width }],
|
||||
"Sets the minimum width of the element. [Docs](https://tailwindcss.com/docs/min-width)",
|
||||
),
|
||||
// TODO: These don't use the same size ramp as the others
|
||||
// see https://tailwindcss.com/docs/max-width
|
||||
(
|
||||
"min_h",
|
||||
true,
|
||||
vec![quote! { min_size.height }],
|
||||
"Sets the minimum height of the element. [Docs](https://tailwindcss.com/docs/min-height)",
|
||||
),
|
||||
// TODO: These don't use the same size ramp as the others
|
||||
// see https://tailwindcss.com/docs/max-width
|
||||
(
|
||||
"max_w",
|
||||
true,
|
||||
vec![quote! { max_size.width }],
|
||||
"Sets the maximum width of the element. [Docs](https://tailwindcss.com/docs/max-width)",
|
||||
),
|
||||
// TODO: These don't use the same size ramp as the others
|
||||
// see https://tailwindcss.com/docs/max-width
|
||||
(
|
||||
"max_h",
|
||||
true,
|
||||
vec![quote! { max_size.height }],
|
||||
"Sets the maximum height of the element. [Docs](https://tailwindcss.com/docs/max-height)",
|
||||
),
|
||||
(
|
||||
"m",
|
||||
true,
|
||||
vec![
|
||||
quote! { margin.top },
|
||||
quote! { margin.bottom },
|
||||
quote! { margin.left },
|
||||
quote! { margin.right },
|
||||
],
|
||||
"Sets the margin of the element. [Docs](https://tailwindcss.com/docs/margin)"
|
||||
),
|
||||
("mt", true, vec![quote! { margin.top }], "Sets the top margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"),
|
||||
(
|
||||
"mb",
|
||||
true,
|
||||
vec![quote! { margin.bottom }],
|
||||
"Sets the bottom margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"
|
||||
),
|
||||
(
|
||||
"my",
|
||||
true,
|
||||
vec![quote! { margin.top }, quote! { margin.bottom }],
|
||||
"Sets the vertical margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-vertical-margin)"
|
||||
),
|
||||
(
|
||||
"mx",
|
||||
true,
|
||||
vec![quote! { margin.left }, quote! { margin.right }],
|
||||
"Sets the horizontal margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-horizontal-margin)"
|
||||
),
|
||||
("ml", true, vec![quote! { margin.left }], "Sets the left margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"),
|
||||
(
|
||||
"mr",
|
||||
true,
|
||||
vec![quote! { margin.right }],
|
||||
"Sets the right margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"
|
||||
),
|
||||
(
|
||||
"p",
|
||||
false,
|
||||
vec![
|
||||
quote! { padding.top },
|
||||
quote! { padding.bottom },
|
||||
quote! { padding.left },
|
||||
quote! { padding.right },
|
||||
],
|
||||
"Sets the padding of the element. [Docs](https://tailwindcss.com/docs/padding)"
|
||||
),
|
||||
(
|
||||
"pt",
|
||||
false,
|
||||
vec![quote! { padding.top }],
|
||||
"Sets the top padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
|
||||
),
|
||||
(
|
||||
"pb",
|
||||
false,
|
||||
vec![quote! { padding.bottom }],
|
||||
"Sets the bottom padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
|
||||
),
|
||||
(
|
||||
"px",
|
||||
false,
|
||||
vec![quote! { padding.left }, quote! { padding.right }],
|
||||
"Sets the horizontal padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-horizontal-padding)"
|
||||
),
|
||||
(
|
||||
"py",
|
||||
false,
|
||||
vec![quote! { padding.top }, quote! { padding.bottom }],
|
||||
"Sets the vertical padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-vertical-padding)"
|
||||
),
|
||||
(
|
||||
"pl",
|
||||
false,
|
||||
vec![quote! { padding.left }],
|
||||
"Sets the left padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
|
||||
),
|
||||
(
|
||||
"pr",
|
||||
false,
|
||||
vec![quote! { padding.right }],
|
||||
"Sets the right padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
|
||||
),
|
||||
(
|
||||
"inset",
|
||||
true,
|
||||
vec![quote! { inset.top }, quote! { inset.right }, quote! { inset.bottom }, quote! { inset.left }],
|
||||
"Sets the top, right, bottom, and left values of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
|
||||
),
|
||||
(
|
||||
"top",
|
||||
true,
|
||||
vec![quote! { inset.top }],
|
||||
"Sets the top value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
|
||||
),
|
||||
(
|
||||
"bottom",
|
||||
true,
|
||||
vec![quote! { inset.bottom }],
|
||||
"Sets the bottom value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
|
||||
),
|
||||
(
|
||||
"left",
|
||||
true,
|
||||
vec![quote! { inset.left }],
|
||||
"Sets the left value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
|
||||
),
|
||||
(
|
||||
"right",
|
||||
true,
|
||||
vec![quote! { inset.right }],
|
||||
"Sets the right value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
|
||||
),
|
||||
(
|
||||
"gap",
|
||||
false,
|
||||
vec![quote! { gap.width }, quote! { gap.height }],
|
||||
"Sets the gap between rows and columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap)"
|
||||
),
|
||||
(
|
||||
"gap_x",
|
||||
false,
|
||||
vec![quote! { gap.width }],
|
||||
"Sets the gap between columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)"
|
||||
),
|
||||
(
|
||||
"gap_y",
|
||||
false,
|
||||
vec![quote! { gap.height }],
|
||||
"Sets the gap between rows in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)"
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns a vec of (Suffix size, tokens that correspond to this size, documentation)
|
||||
fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
|
||||
vec![
|
||||
("0", quote! { px(0.) }, "0px"),
|
||||
("0p5", quote! { rems(0.125) }, "2px (0.125rem)"),
|
||||
("1", quote! { rems(0.25) }, "4px (0.25rem)"),
|
||||
("1p5", quote! { rems(0.375) }, "6px (0.375rem)"),
|
||||
("2", quote! { rems(0.5) }, "8px (0.5rem)"),
|
||||
("2p5", quote! { rems(0.625) }, "10px (0.625rem)"),
|
||||
("3", quote! { rems(0.75) }, "12px (0.75rem)"),
|
||||
("3p5", quote! { rems(0.875) }, "14px (0.875rem)"),
|
||||
("4", quote! { rems(1.) }, "16px (1rem)"),
|
||||
("5", quote! { rems(1.25) }, "20px (1.25rem)"),
|
||||
("6", quote! { rems(1.5) }, "24px (1.5rem)"),
|
||||
("7", quote! { rems(1.75) }, "28px (1.75rem)"),
|
||||
("8", quote! { rems(2.0) }, "32px (2rem)"),
|
||||
("9", quote! { rems(2.25) }, "36px (2.25rem)"),
|
||||
("10", quote! { rems(2.5) }, "40px (2.5rem)"),
|
||||
("11", quote! { rems(2.75) }, "44px (2.75rem)"),
|
||||
("12", quote! { rems(3.) }, "48px (3rem)"),
|
||||
("16", quote! { rems(4.) }, "64px (4rem)"),
|
||||
("20", quote! { rems(5.) }, "80px (5rem)"),
|
||||
("24", quote! { rems(6.) }, "96px (6rem)"),
|
||||
("32", quote! { rems(8.) }, "128px (8rem)"),
|
||||
("40", quote! { rems(10.) }, "160px (10rem)"),
|
||||
("48", quote! { rems(12.) }, "192px (12rem)"),
|
||||
("56", quote! { rems(14.) }, "224px (14rem)"),
|
||||
("64", quote! { rems(16.) }, "256px (16rem)"),
|
||||
("72", quote! { rems(18.) }, "288px (18rem)"),
|
||||
("80", quote! { rems(20.) }, "320px (20rem)"),
|
||||
("96", quote! { rems(24.) }, "384px (24rem)"),
|
||||
("112", quote! { rems(28.) }, "448px (28rem)"),
|
||||
("128", quote! { rems(32.) }, "512px (32rem)"),
|
||||
("auto", quote! { auto() }, "Auto"),
|
||||
("px", quote! { px(1.) }, "1px"),
|
||||
("full", quote! { relative(1.) }, "100%"),
|
||||
("1_2", quote! { relative(0.5) }, "50% (1/2)"),
|
||||
("1_3", quote! { relative(1./3.) }, "33% (1/3)"),
|
||||
("2_3", quote! { relative(2./3.) }, "66% (2/3)"),
|
||||
("1_4", quote! { relative(0.25) }, "25% (1/4)"),
|
||||
("2_4", quote! { relative(0.5) }, "50% (2/4)"),
|
||||
("3_4", quote! { relative(0.75) }, "75% (3/4)"),
|
||||
("1_5", quote! { relative(0.2) }, "20% (1/5)"),
|
||||
("2_5", quote! { relative(0.4) }, "40% (2/5)"),
|
||||
("3_5", quote! { relative(0.6) }, "60% (3/5)"),
|
||||
("4_5", quote! { relative(0.8) }, "80% (4/5)"),
|
||||
("1_6", quote! { relative(1./6.) }, "16% (1/6)"),
|
||||
("5_6", quote! { relative(5./6.) }, "80% (5/6)"),
|
||||
("1_12", quote! { relative(1./12.) }, "8% (1/12)"),
|
||||
]
|
||||
}
|
||||
|
||||
fn corner_prefixes() -> Vec<(&'static str, Vec<TokenStream2>, &'static str)> {
|
||||
vec![
|
||||
(
|
||||
"rounded",
|
||||
vec![
|
||||
quote! { corner_radii.top_left },
|
||||
quote! { corner_radii.top_right },
|
||||
quote! { corner_radii.bottom_right },
|
||||
quote! { corner_radii.bottom_left },
|
||||
],
|
||||
"Sets the border radius of the element. [Docs](https://tailwindcss.com/docs/border-radius)"
|
||||
),
|
||||
(
|
||||
"rounded_t",
|
||||
vec![
|
||||
quote! { corner_radii.top_left },
|
||||
quote! { corner_radii.top_right },
|
||||
],
|
||||
"Sets the border radius of the top side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)"
|
||||
),
|
||||
(
|
||||
"rounded_b",
|
||||
vec![
|
||||
quote! { corner_radii.bottom_left },
|
||||
quote! { corner_radii.bottom_right },
|
||||
],
|
||||
"Sets the border radius of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)"
|
||||
),
|
||||
(
|
||||
"rounded_r",
|
||||
vec![
|
||||
quote! { corner_radii.top_right },
|
||||
quote! { corner_radii.bottom_right },
|
||||
],
|
||||
"Sets the border radius of the right side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)"
|
||||
),
|
||||
(
|
||||
"rounded_l",
|
||||
vec![
|
||||
quote! { corner_radii.top_left },
|
||||
quote! { corner_radii.bottom_left },
|
||||
],
|
||||
"Sets the border radius of the left side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)"
|
||||
),
|
||||
(
|
||||
"rounded_tl",
|
||||
vec![quote! { corner_radii.top_left }],
|
||||
"Sets the border radius of the top left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)"
|
||||
),
|
||||
(
|
||||
"rounded_tr",
|
||||
vec![quote! { corner_radii.top_right }],
|
||||
"Sets the border radius of the top right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)"
|
||||
),
|
||||
(
|
||||
"rounded_bl",
|
||||
vec![quote! { corner_radii.bottom_left }],
|
||||
"Sets the border radius of the bottom left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)"
|
||||
),
|
||||
(
|
||||
"rounded_br",
|
||||
vec![quote! { corner_radii.bottom_right }],
|
||||
"Sets the border radius of the bottom right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)"
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn corner_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
|
||||
vec![
|
||||
("none", quote! { px(0.) }, "0px"),
|
||||
("sm", quote! { rems(0.125) }, "2px (0.125rem)"),
|
||||
("md", quote! { rems(0.25) }, "4px (0.25rem)"),
|
||||
("lg", quote! { rems(0.5) }, "8px (0.5rem)"),
|
||||
("xl", quote! { rems(0.75) }, "12px (0.75rem)"),
|
||||
("2xl", quote! { rems(1.) }, "16px (1rem)"),
|
||||
("3xl", quote! { rems(1.5) }, "24px (1.5rem)"),
|
||||
("full", quote! { px(9999.) }, "9999px"),
|
||||
]
|
||||
}
|
||||
|
||||
fn border_prefixes() -> Vec<(&'static str, Vec<TokenStream2>, &'static str)> {
|
||||
vec![
|
||||
(
|
||||
"border",
|
||||
vec![
|
||||
quote! { border_widths.top },
|
||||
quote! { border_widths.right },
|
||||
quote! { border_widths.bottom },
|
||||
quote! { border_widths.left },
|
||||
],
|
||||
"Sets the border width of the element. [Docs](https://tailwindcss.com/docs/border-width)"
|
||||
),
|
||||
(
|
||||
"border_t",
|
||||
vec![quote! { border_widths.top }],
|
||||
"Sets the border width of the top side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)"
|
||||
),
|
||||
(
|
||||
"border_b",
|
||||
vec![quote! { border_widths.bottom }],
|
||||
"Sets the border width of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)"
|
||||
),
|
||||
(
|
||||
"border_r",
|
||||
vec![quote! { border_widths.right }],
|
||||
"Sets the border width of the right side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)"
|
||||
),
|
||||
(
|
||||
"border_l",
|
||||
vec![quote! { border_widths.left }],
|
||||
"Sets the border width of the left side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)"
|
||||
),
|
||||
(
|
||||
"border_x",
|
||||
vec![
|
||||
quote! { border_widths.left },
|
||||
quote! { border_widths.right },
|
||||
],
|
||||
"Sets the border width of the vertical sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)"
|
||||
),
|
||||
(
|
||||
"border_y",
|
||||
vec![
|
||||
quote! { border_widths.top },
|
||||
quote! { border_widths.bottom },
|
||||
],
|
||||
"Sets the border width of the horizontal sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)"
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn border_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
|
||||
vec![
|
||||
("0", quote! { px(0.)}, "0px"),
|
||||
("1", quote! { px(1.) }, "1px"),
|
||||
("2", quote! { px(2.) }, "2px"),
|
||||
("3", quote! { px(3.) }, "3px"),
|
||||
("4", quote! { px(4.) }, "4px"),
|
||||
("5", quote! { px(5.) }, "5px"),
|
||||
("6", quote! { px(6.) }, "6px"),
|
||||
("7", quote! { px(7.) }, "7px"),
|
||||
("8", quote! { px(8.) }, "8px"),
|
||||
("9", quote! { px(9.) }, "9px"),
|
||||
("10", quote! { px(10.) }, "10px"),
|
||||
("11", quote! { px(11.) }, "11px"),
|
||||
("12", quote! { px(12.) }, "12px"),
|
||||
("16", quote! { px(16.) }, "16px"),
|
||||
("20", quote! { px(20.) }, "20px"),
|
||||
("24", quote! { px(24.) }, "24px"),
|
||||
("32", quote! { px(32.) }, "32px"),
|
||||
]
|
||||
}
|
||||
1374
crates/gpui_macros/src/styles.rs
Normal file
1374
crates/gpui_macros/src/styles.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ use gpui::{
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::ops::Range;
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use theme::{color_alpha, ActiveTheme, ThemeSettings};
|
||||
|
||||
/// An outline of all the symbols contained in a buffer.
|
||||
#[derive(Debug)]
|
||||
@@ -146,9 +146,15 @@ impl<T> Outline<T> {
|
||||
|
||||
pub fn render_item<T>(
|
||||
outline_item: &OutlineItem<T>,
|
||||
custom_highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||
match_ranges: impl IntoIterator<Item = Range<usize>>,
|
||||
cx: &AppContext,
|
||||
) -> StyledText {
|
||||
let mut highlight_style = HighlightStyle::default();
|
||||
highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3));
|
||||
let custom_highlights = match_ranges
|
||||
.into_iter()
|
||||
.map(|range| (range, highlight_style));
|
||||
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
// TODO: We probably shouldn't need to build a whole new text style here
|
||||
|
||||
@@ -317,8 +317,11 @@ impl LspAdapter for NodeVersionAdapter {
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = latest_version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||||
let destination_path =
|
||||
container_dir.join(format!("package-version-server-{}", version.name));
|
||||
let destination_path = container_dir.join(format!(
|
||||
"package-version-server-{}{}",
|
||||
version.name,
|
||||
std::env::consts::EXE_SUFFIX
|
||||
));
|
||||
let destination_container_path =
|
||||
container_dir.join(format!("package-version-server-{}-tmp", version.name));
|
||||
if fs::metadata(&destination_path).await.is_err() {
|
||||
@@ -340,7 +343,10 @@ impl LspAdapter for NodeVersionAdapter {
|
||||
}
|
||||
|
||||
fs::copy(
|
||||
destination_container_path.join("package-version-server"),
|
||||
destination_container_path.join(format!(
|
||||
"package-version-server{}",
|
||||
std::env::consts::EXE_SUFFIX
|
||||
)),
|
||||
&destination_path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -9,7 +9,7 @@ brackets = [
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
{ start = "'", end = "'", close = false, newline = false, not_in = ["string"] },
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
|
||||
auto_indent_using_last_non_empty_line = false
|
||||
|
||||
@@ -18,6 +18,9 @@ use std::{
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server.ps1";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
@@ -108,11 +111,39 @@ impl LspAdapter for TailwindLspAdapter {
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut env_path = vec![self
|
||||
.node
|
||||
.binary_path()
|
||||
.await?
|
||||
.parent()
|
||||
.expect("invalid node binary path")
|
||||
.to_path_buf()];
|
||||
|
||||
if let Some(existing_path) = std::env::var_os("PATH") {
|
||||
let mut paths = std::env::split_paths(&existing_path).collect::<Vec<_>>();
|
||||
env_path.append(&mut paths);
|
||||
}
|
||||
|
||||
let env_path = std::env::join_paths(env_path)?;
|
||||
let mut env = HashMap::default();
|
||||
env.insert("PATH".to_string(), env_path.to_string_lossy().to_string());
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: "powershell.exe".into(),
|
||||
env: Some(env),
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
@@ -158,26 +189,25 @@ impl LspAdapter for TailwindLspAdapter {
|
||||
.unwrap_or_default()
|
||||
})?;
|
||||
|
||||
// We need to set this to null if it's not set, because tailwindcss-languageserver
|
||||
// will check whether it's an object and if it is (even if it's empty) it will
|
||||
// ignore the `userLanguages` from the initialization options.
|
||||
let include_languages = tailwind_user_settings
|
||||
.get("includeLanguages")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null);
|
||||
|
||||
let experimental = tailwind_user_settings
|
||||
.get("experimental")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!([]));
|
||||
|
||||
Ok(json!({
|
||||
let mut configuration = json!({
|
||||
"tailwindCSS": {
|
||||
"emmetCompletions": true,
|
||||
"includeLanguages": include_languages,
|
||||
"experimental": experimental,
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
if let Some(experimental) = tailwind_user_settings.get("experimental").cloned() {
|
||||
configuration["tailwindCSS"]["experimental"] = experimental;
|
||||
}
|
||||
|
||||
if let Some(class_attributes) = tailwind_user_settings.get("classAttributes").cloned() {
|
||||
configuration["tailwindCSS"]["classAttributes"] = class_attributes;
|
||||
}
|
||||
|
||||
if let Some(include_languages) = tailwind_user_settings.get("includeLanguages").cloned() {
|
||||
configuration["tailwindCSS"]["includeLanguages"] = include_languages;
|
||||
}
|
||||
|
||||
Ok(configuration)
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<String, String> {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../typescript/brackets.scm
|
||||
5
crates/languages/src/tsx/brackets.scm
Normal file
5
crates/languages/src/tsx/brackets.scm
Normal file
@@ -0,0 +1,5 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
@@ -1 +0,0 @@
|
||||
../typescript/indents.scm
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user