Compare commits
103 Commits
paint-pre-
...
fix-hover-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30239b3cc6 | ||
|
|
180421fe5a | ||
|
|
1a13995b8f | ||
|
|
b6379a9177 | ||
|
|
2cb041504b | ||
|
|
4d8dc79d7e | ||
|
|
474c806331 | ||
|
|
2db6ccd803 | ||
|
|
2d6a227258 | ||
|
|
a3ce933b04 | ||
|
|
816c48b7d6 | ||
|
|
42ac9880c6 | ||
|
|
65318cb6ac | ||
|
|
71557f3eb3 | ||
|
|
a588f674db | ||
|
|
50dd38bd02 | ||
|
|
caa156ab13 | ||
|
|
a82f4857f4 | ||
|
|
0de8672044 | ||
|
|
cc8e3c2286 | ||
|
|
347f68887f | ||
|
|
a475d8640f | ||
|
|
991c9ec441 | ||
|
|
250df707bf | ||
|
|
ba6b319046 | ||
|
|
bd94a0e921 | ||
|
|
40bbd0031d | ||
|
|
946f4a312a | ||
|
|
af06063d31 | ||
|
|
5c4f3c0cea | ||
|
|
c6826a61a0 | ||
|
|
fa2c92d190 | ||
|
|
20b10fdca9 | ||
|
|
4f40d3c801 | ||
|
|
b716035d02 | ||
|
|
94bc216bbd | ||
|
|
95d5ea7edc | ||
|
|
aff858bd00 | ||
|
|
583d85cf66 | ||
|
|
36586b77ec | ||
|
|
587788b9a0 | ||
|
|
6f36527bc6 | ||
|
|
aa34e306f7 | ||
|
|
e5d971f4c7 | ||
|
|
38c3a93f0c | ||
|
|
f930969411 | ||
|
|
266bb62813 | ||
|
|
6e897d9969 | ||
|
|
d90b052162 | ||
|
|
49a53e7654 | ||
|
|
3220986fc9 | ||
|
|
9fcda5a5ac | ||
|
|
5e43290aa1 | ||
|
|
f895d66d1c | ||
|
|
7bf16f263e | ||
|
|
d3745a3931 | ||
|
|
0c939e5dfc | ||
|
|
b9151b9506 | ||
|
|
2679457b02 | ||
|
|
45e2c01773 | ||
|
|
fd9823898f | ||
|
|
d5aba2795b | ||
|
|
92b2e5608b | ||
|
|
c58d72ea2b | ||
|
|
58a5a1eb8f | ||
|
|
cd640a87a9 | ||
|
|
c97ecc7326 | ||
|
|
48f0f387f8 | ||
|
|
2ec910f772 | ||
|
|
8a73bc4c7d | ||
|
|
8f5d7db875 | ||
|
|
389d26d974 | ||
|
|
e580e2ff0a | ||
|
|
3d9503a454 | ||
|
|
5c7cec9f85 | ||
|
|
b028231aea | ||
|
|
f7d2cb1818 | ||
|
|
78dcd72790 | ||
|
|
d51a0b60be | ||
|
|
8178d347b6 | ||
|
|
33ecb424af | ||
|
|
91b97387b6 | ||
|
|
0a40a21c74 | ||
|
|
b14d576349 | ||
|
|
db0eaca2e5 | ||
|
|
80db468720 | ||
|
|
0d2ad67b27 | ||
|
|
7065d6c46d | ||
|
|
6c714c13b3 | ||
|
|
c54d6aff6c | ||
|
|
48a6fb9e84 | ||
|
|
e9f400a8bd | ||
|
|
fc101c1fb3 | ||
|
|
4616d66e1d | ||
|
|
3ef8a9910d | ||
|
|
1e44bac418 | ||
|
|
aad7761038 | ||
|
|
0422d43798 | ||
|
|
b00b65b330 | ||
|
|
fddb778e5f | ||
|
|
77974a4367 | ||
|
|
0037f0b2fd | ||
|
|
37f6a706cc |
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -64,6 +64,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
with:
|
||||
version: v1.29.0
|
||||
- uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
input: "crates/rpc/proto/"
|
||||
@@ -115,7 +117,6 @@ jobs:
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-
|
||||
|
||||
- name: configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -127,6 +128,36 @@ jobs:
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
# todo!(windows): Actually run the tests
|
||||
windows_tests:
|
||||
name: (Windows) Run Clippy and tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Restore from cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
# todo!(windows): Actually run clippy
|
||||
#- name: cargo clippy
|
||||
# shell: bash -euxo pipefail {0}
|
||||
# run: script/clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
bundle:
|
||||
name: Bundle macOS app
|
||||
runs-on:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"languages": {
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"TOML": {
|
||||
"formatter": "prettier",
|
||||
"format_on_save": "off"
|
||||
|
||||
190
Cargo.lock
generated
190
Cargo.lock
generated
@@ -631,6 +631,7 @@ dependencies = [
|
||||
"async-global-executor",
|
||||
"async-io 1.13.0",
|
||||
"async-lock 2.8.0",
|
||||
"async-process",
|
||||
"crossbeam-utils",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -769,10 +770,12 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"db",
|
||||
"editor",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
"project",
|
||||
"release_channel",
|
||||
@@ -1348,7 +1351,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.48",
|
||||
"which",
|
||||
"which 4.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1908,6 +1911,7 @@ dependencies = [
|
||||
"async-recursion 0.3.2",
|
||||
"async-tungstenite",
|
||||
"chrono",
|
||||
"clock",
|
||||
"collections",
|
||||
"db",
|
||||
"feature_flags",
|
||||
@@ -1945,6 +1949,8 @@ dependencies = [
|
||||
name = "clock"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"parking_lot 0.11.2",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -2090,6 +2096,7 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"extensions_ui",
|
||||
"feature_flags",
|
||||
"feedback",
|
||||
"futures 0.3.28",
|
||||
@@ -2182,6 +2189,7 @@ dependencies = [
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
"postage",
|
||||
"project",
|
||||
"release_channel",
|
||||
"serde",
|
||||
@@ -2249,6 +2257,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"async-tar",
|
||||
"clock",
|
||||
"collections",
|
||||
@@ -3672,6 +3681,7 @@ dependencies = [
|
||||
"text",
|
||||
"time",
|
||||
"util",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4115,6 +4125,7 @@ dependencies = [
|
||||
"pathfinder_geometry",
|
||||
"png",
|
||||
"postage",
|
||||
"profiling",
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle 0.5.2",
|
||||
"raw-window-handle 0.6.0",
|
||||
@@ -4337,11 +4348,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.5"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
|
||||
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5850,15 +5861,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
@@ -6858,14 +6860,33 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "procinfo"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6bb6512223e69d5efc64d#5cd757e5f2eb039ed0c6bb6512223e69d5efc64d"
|
||||
source = "git+https://github.com/zed-industries/wezterm?rev=0c13436f4fa8b126f46dd4a20106419b41666897#0c13436f4fa8b126f46dd4a20106419b41666897"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"ntapi 0.3.7",
|
||||
"ntapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
|
||||
dependencies = [
|
||||
"profiling-procmacros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling-procmacros"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
@@ -6904,7 +6925,6 @@ dependencies = [
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"runnable",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -6914,6 +6934,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"task",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"text",
|
||||
@@ -6921,6 +6942,7 @@ dependencies = [
|
||||
"toml 0.8.10",
|
||||
"unindent",
|
||||
"util",
|
||||
"which 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7030,7 +7052,7 @@ dependencies = [
|
||||
"prost-types 0.9.0",
|
||||
"regex",
|
||||
"tempfile",
|
||||
"which",
|
||||
"which 4.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7322,6 +7344,7 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"ordered-float 2.10.0",
|
||||
"picker",
|
||||
"postage",
|
||||
@@ -7562,6 +7585,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"linkify",
|
||||
"pulldown-cmark",
|
||||
"smallvec",
|
||||
"smol",
|
||||
@@ -7761,48 +7785,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runnable"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"parking_lot 0.11.2",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runnables_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"db",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"log",
|
||||
"picker",
|
||||
"project",
|
||||
"runnable",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.29.0"
|
||||
@@ -9340,7 +9322,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"core-foundation-sys 0.8.6",
|
||||
"libc",
|
||||
"ntapi 0.4.1",
|
||||
"ntapi",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"winapi 0.3.9",
|
||||
@@ -9375,6 +9357,49 @@ version = "0.12.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae"
|
||||
|
||||
[[package]]
|
||||
name = "task"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"parking_lot 0.11.2",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tasks_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"db",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"log",
|
||||
"menu",
|
||||
"picker",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"task",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.9.0"
|
||||
@@ -9426,7 +9451,6 @@ dependencies = [
|
||||
"ordered-float 2.10.0",
|
||||
"procinfo",
|
||||
"rand 0.8.5",
|
||||
"runnable",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -9435,6 +9459,7 @@ dependencies = [
|
||||
"shellexpand",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"task",
|
||||
"theme",
|
||||
"thiserror",
|
||||
"util",
|
||||
@@ -9461,7 +9486,6 @@ dependencies = [
|
||||
"procinfo",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"runnable",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -9470,6 +9494,7 @@ dependencies = [
|
||||
"shellexpand",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"task",
|
||||
"terminal",
|
||||
"theme",
|
||||
"thiserror",
|
||||
@@ -10195,7 +10220,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-gitcommit"
|
||||
version = "0.3.3"
|
||||
source = "git+https://github.com/gbprod/tree-sitter-gitcommit#e8d9eda4e5ea0b08aa39d48dab0f6553058fbe0f"
|
||||
source = "git+https://github.com/gbprod/tree-sitter-gitcommit#7c01af8d227b5344f62aade2ff00f19bd0c458ca"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -10249,7 +10274,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-haskell"
|
||||
version = "0.14.0"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-haskell?rev=cf98de23e4285b8e6bcb57b050ef2326e2cc284b#cf98de23e4285b8e6bcb57b050ef2326e2cc284b"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-haskell?rev=8a99848fc734f9c4ea523b3f2a07df133cbbcec2#8a99848fc734f9c4ea523b3f2a07df133cbbcec2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -10333,7 +10358,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-nu"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/nushell/tree-sitter-nu?rev=26bbaecda0039df4067861ab38ea8ea169f7f5aa#26bbaecda0039df4067861ab38ea8ea169f7f5aa"
|
||||
source = "git+https://github.com/nushell/tree-sitter-nu?rev=7dd29f9616822e5fc259f5b4ae6c4ded9a71a132#7dd29f9616822e5fc259f5b4ae6c4ded9a71a132"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -10378,8 +10403,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-purescript"
|
||||
version = "1.0.0"
|
||||
source = "git+https://github.com/ivanmoreau/tree-sitter-purescript?rev=a37140f0c7034977b90faa73c94fcb8a5e45ed08#a37140f0c7034977b90faa73c94fcb8a5e45ed08"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/postsolar/tree-sitter-purescript?rev=v0.1.0#0554811a512b9cec08b5a83ce9096eb22da18213"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -10436,7 +10461,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-svelte"
|
||||
version = "0.10.2"
|
||||
source = "git+https://github.com/Himujjal/tree-sitter-svelte?rev=697bb515471871e85ff799ea57a76298a71a9cca#697bb515471871e85ff799ea57a76298a71a9cca"
|
||||
source = "git+https://github.com/Himujjal/tree-sitter-svelte?rev=bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd#bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -10462,8 +10487,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-uiua"
|
||||
version = "0.3.3"
|
||||
source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=21dc2db39494585bf29a3f86d5add6e9d11a22ba#21dc2db39494585bf29a3f86d5add6e9d11a22ba"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -10904,6 +10929,7 @@ dependencies = [
|
||||
"project",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -11400,6 +11426,19 @@ dependencies = [
|
||||
"rustix 0.38.30",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"once_cell",
|
||||
"rustix 0.38.30",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.4.1"
|
||||
@@ -11711,6 +11750,7 @@ dependencies = [
|
||||
"bincode",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"db",
|
||||
"derive_more",
|
||||
@@ -11728,7 +11768,6 @@ dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project",
|
||||
"runnable",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -11736,6 +11775,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"sqlez",
|
||||
"task",
|
||||
"terminal",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -11915,7 +11955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.124.0"
|
||||
version = "0.125.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@@ -11935,6 +11975,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"cli",
|
||||
"client",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette",
|
||||
@@ -11978,6 +12019,7 @@ dependencies = [
|
||||
"outline",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"profiling",
|
||||
"project",
|
||||
"project_panel",
|
||||
"project_symbols",
|
||||
@@ -11989,8 +12031,6 @@ dependencies = [
|
||||
"rope",
|
||||
"rpc",
|
||||
"rsa 0.4.0",
|
||||
"runnable",
|
||||
"runnables_ui",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"search",
|
||||
@@ -12004,6 +12044,8 @@ dependencies = [
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"tempfile",
|
||||
"terminal_view",
|
||||
"text",
|
||||
|
||||
21
Cargo.toml
21
Cargo.toml
@@ -63,8 +63,8 @@ members = [
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/runnable",
|
||||
"crates/runnables_ui",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/settings",
|
||||
@@ -155,8 +155,8 @@ release_channel = { path = "crates/release_channel" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
runnable = { path = "crates/runnable" }
|
||||
runnables_ui = { path = "crates/runnables_ui" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
settings = { path = "crates/settings" }
|
||||
@@ -199,9 +199,11 @@ indoc = "1"
|
||||
# We explicitly disable a http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
|
||||
lazy_static = "1.4.0"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = "2.1.1"
|
||||
parking_lot = "0.11.1"
|
||||
profiling = "1"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
prost = "0.8"
|
||||
@@ -247,7 +249,7 @@ tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "cf98de23e4285b8e6bcb57b050ef2326e2cc284b" }
|
||||
tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "8a99848fc734f9c4ea523b3f2a07df133cbbcec2" }
|
||||
tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
@@ -255,21 +257,21 @@ tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", re
|
||||
tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }
|
||||
tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" }
|
||||
tree-sitter-php = "0.21.1"
|
||||
tree-sitter-prisma-io = { git = "https://github.com/victorhqc/tree-sitter-prisma" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" }
|
||||
tree-sitter-purescript = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "v0.1.0" }
|
||||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }
|
||||
tree-sitter-ruby = "0.20.0"
|
||||
tree-sitter-rust = "0.20.3"
|
||||
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" }
|
||||
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "697bb515471871e85ff799ea57a76298a71a9cca" }
|
||||
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" }
|
||||
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
|
||||
tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" }
|
||||
tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "21dc2db39494585bf29a3f86d5add6e9d11a22ba" }
|
||||
tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
|
||||
tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
|
||||
@@ -277,6 +279,7 @@ unindent = "0.1.7"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
wasmtime = "16"
|
||||
which = "6.0.0"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
@@ -21,7 +21,8 @@ brew install zed
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed](./docs/src/developing_zed__building_zed.md)
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
561
assets/keymaps/default-linux.json
Normal file
561
assets/keymaps/default-linux.json
Normal file
@@ -0,0 +1,561 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"ctrl-up": "menu::SelectFirst",
|
||||
"ctrl-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"shift-f10": "menu::ShowContextMenu",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"ctrl-o": "workspace::Open",
|
||||
"ctrl-=": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-+": "zed::IncreaseBufferFontSize",
|
||||
"ctrl--": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-0": "zed::ResetBufferFontSize",
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"ctrl-h": "zed::Hide",
|
||||
"alt-ctrl-h": "zed::HideOthers",
|
||||
"ctrl-m": "zed::Minimize",
|
||||
"f11": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"backspace": "editor::Backspace",
|
||||
"shift-backspace": "editor::Backspace",
|
||||
"ctrl-h": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
"ctrl-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-delete": "editor::DeleteToEndOfLine",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-h": "editor::DeleteToPreviousWordStart",
|
||||
"alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
"ctrl-up": "editor::MoveToStartOfParagraph",
|
||||
"pageup": "editor::PageUp",
|
||||
"shift-pageup": "editor::MovePageUp",
|
||||
"home": "editor::MoveToBeginningOfLine",
|
||||
"down": "editor::MoveDown",
|
||||
"ctrl-down": "editor::MoveToEndOfParagraph",
|
||||
"pagedown": "editor::PageDown",
|
||||
"shift-pagedown": "editor::MovePageDown",
|
||||
"end": "editor::MoveToEndOfLine",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-shift-l": "editor::NextScreen", // todo!(linux): What is this
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
"alt-f": "editor::MoveToNextWordEnd",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-=end": "editor::MoveToEnd",
|
||||
"shift-up": "editor::SelectUp",
|
||||
"ctrl-shift-p": "editor::SelectUp",
|
||||
"shift-down": "editor::SelectDown",
|
||||
"ctrl-shift-n": "editor::SelectDown",
|
||||
"shift-left": "editor::SelectLeft",
|
||||
"ctrl-shift-b": "editor::SelectLeft",
|
||||
"shift-right": "editor::SelectRight",
|
||||
"ctrl-shift-f": "editor::SelectRight",
|
||||
"alt-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd",
|
||||
"alt-shift-f": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"ctrl-l": "editor::SelectLine",
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
"shift-home": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"shift-end": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-e": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-f": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
"alt-\\": "copilot::Suggest",
|
||||
"alt-]": "copilot::NextSuggestion",
|
||||
"alt-[": "copilot::PreviousSuggestion",
|
||||
"ctrl->": "assistant::QuoteSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == auto_height",
|
||||
"bindings": {
|
||||
"ctrl-enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"ctrl-shift-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"escape": "buffer_search::Dismiss",
|
||||
"tab": "buffer_search::FocusEditor",
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-tab": "search::CycleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"ctrl-enter": "search::ReplaceAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace > Editor",
|
||||
"bindings": {
|
||||
"up": "search::PreviousHistoryQuery",
|
||||
"down": "search::NextHistoryQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar > Editor",
|
||||
"bindings": {
|
||||
"up": "search::PreviousHistoryQuery",
|
||||
"down": "search::NextHistoryQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"ctrl-enter": "search::ReplaceAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchView",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-{": "pane::ActivatePrevItem",
|
||||
"ctrl-}": "pane::ActivateNextItem",
|
||||
"ctrl-alt-left": "pane::ActivatePrevItem",
|
||||
"ctrl-alt-right": "pane::ActivateNextItem",
|
||||
"ctrl-w": "pane::CloseActiveItem",
|
||||
"ctrl-alt-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-shift-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-k u": "pane::CloseCleanItems",
|
||||
"ctrl-k ctrl-w": "pane::CloseAllItems",
|
||||
"ctrl-f": "project_search::ToggleFocus",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"ctrl-alt-c": "search::ToggleCaseSensitive",
|
||||
"ctrl-alt-w": "search::ToggleWholeWord",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-alt-f": "project_search::ToggleFilters",
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"ctrl-alt-up": "editor::AddSelectionAbove",
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"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-k ctrl-i": "editor::Hover",
|
||||
"ctrl-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": false
|
||||
}
|
||||
],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPrevDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"ctrl-f12": "editor::GoToTypeDefinition",
|
||||
"ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-alt-[": "editor::Fold",
|
||||
"ctrl-alt-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"ctrl-alt-r": "editor::RevealInFinder",
|
||||
"ctrl-alt-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-1": ["pane::ActivateItem", 0],
|
||||
"ctrl-2": ["pane::ActivateItem", 1],
|
||||
"ctrl-3": ["pane::ActivateItem", 2],
|
||||
"ctrl-4": ["pane::ActivateItem", 3],
|
||||
"ctrl-5": ["pane::ActivateItem", 4],
|
||||
"ctrl-6": ["pane::ActivateItem", 5],
|
||||
"ctrl-7": ["pane::ActivateItem", 6],
|
||||
"ctrl-8": ["pane::ActivateItem", 7],
|
||||
"ctrl-9": ["pane::ActivateItem", 8],
|
||||
"ctrl-0": "pane::ActivateLastItem",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"ctrl-_": "pane::GoForward",
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-alt-o": "projects::OpenRecent",
|
||||
"ctrl-alt-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-shift-s": "workspace::SaveAs",
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
"ctrl-3": ["workspace::ActivatePane", 2],
|
||||
"ctrl-4": ["workspace::ActivatePane", 3],
|
||||
"ctrl-5": ["workspace::ActivatePane", 4],
|
||||
"ctrl-6": ["workspace::ActivatePane", 5],
|
||||
"ctrl-7": ["workspace::ActivatePane", 6],
|
||||
"ctrl-8": ["workspace::ActivatePane", 7],
|
||||
"ctrl-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-r": "workspace::ToggleRightDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymap",
|
||||
"ctrl-t": "project_symbols::Toggle",
|
||||
"ctrl-p": "file_finder::Toggle",
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-?": "assistant::ToggleFocus",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
"escape": "workspace::Unfollow",
|
||||
"ctrl-k ctrl-left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-k ctrl-right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-k ctrl-up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-k ctrl-down": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
// todo!(linux) make sure these match linux bindings or remove above comment?
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"ctrl-shift-d": "editor::DuplicateLine",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-alt-up": "editor::MoveLineUp",
|
||||
"ctrl-alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
"ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
|
||||
}
|
||||
},
|
||||
// Bindings from Atom
|
||||
// todo!(linux) make sure these match linux bindings or remove above comment?
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-k up": "pane::SplitUp",
|
||||
"ctrl-k down": "pane::SplitDown",
|
||||
"ctrl-k left": "pane::SplitLeft",
|
||||
"ctrl-k right": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
// Bindings that should be unified with bindings for more general actions
|
||||
{
|
||||
"context": "Editor && renaming",
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmRename"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_code_actions",
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCodeAction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||
"bindings": {
|
||||
"up": "editor::ContextMenuPrev",
|
||||
"ctrl-p": "editor::ContextMenuPrev",
|
||||
"down": "editor::ContextMenuNext",
|
||||
"ctrl-n": "editor::ContextMenuNext",
|
||||
"pageup": "editor::ContextMenuFirst",
|
||||
"pagedown": "editor::ContextMenuLast"
|
||||
}
|
||||
},
|
||||
// Custom bindings
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
||||
// TODO: Move this to a dock open action
|
||||
"ctrl-alt-c": "collab_panel::ToggleFocus",
|
||||
"ctrl-alt-i": "zed::DebugElements",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"ctrl-f8": "editor::GoToHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPrevHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
"ctrl-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-n": "project_panel::NewFile",
|
||||
"ctrl-alt-n": "project_panel::NewDirectory",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"ctrl-alt-c": "project_panel::CopyPath",
|
||||
"ctrl-alt-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete",
|
||||
"ctrl-alt-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(CollabPanel && editing) > Editor",
|
||||
"bindings": {
|
||||
"space": "collab_panel::InsertSpace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal",
|
||||
"bindings": {
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal > Picker > Editor",
|
||||
"bindings": {
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChatPanel > MessageEditor",
|
||||
"bindings": {
|
||||
"escape": "chat_panel::CloseReplyPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-k": "terminal::Clear",
|
||||
// Some nice conveniences
|
||||
"ctrl-backspace": ["terminal::SendText", "\u0015"],
|
||||
"ctrl-right": ["terminal::SendText", "\u0005"],
|
||||
"ctrl-left": ["terminal::SendText", "\u0001"],
|
||||
// Terminal.app compatibility
|
||||
"alt-left": ["terminal::SendText", "\u001bb"],
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||
"down": ["terminal::SendKeystroke", "down"],
|
||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -423,7 +423,9 @@
|
||||
"cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
|
||||
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -529,7 +531,8 @@
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete",
|
||||
"delete": "project_panel::Delete",
|
||||
"cmd-backspace": "project_panel::Delete",
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
}
|
||||
23
assets/keymaps/storybook.json
Normal file
23
assets/keymaps/storybook.json
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
// Standard macOS bindings
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::ShowContextMenu",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-q": "storybook::Quit"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -101,8 +101,14 @@
|
||||
"ctrl-o": "pane::GoBack",
|
||||
"ctrl-i": "pane::GoForward",
|
||||
"ctrl-]": "editor::GoToDefinition",
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"],
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"v": "vim::ToggleVisual",
|
||||
"shift-v": "vim::ToggleVisualLine",
|
||||
"ctrl-v": "vim::ToggleVisualBlock",
|
||||
@@ -235,36 +241,123 @@
|
||||
}
|
||||
],
|
||||
// Count support
|
||||
"1": ["vim::Number", 1],
|
||||
"2": ["vim::Number", 2],
|
||||
"3": ["vim::Number", 3],
|
||||
"4": ["vim::Number", 4],
|
||||
"5": ["vim::Number", 5],
|
||||
"6": ["vim::Number", 6],
|
||||
"7": ["vim::Number", 7],
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9],
|
||||
"1": [
|
||||
"vim::Number",
|
||||
1
|
||||
],
|
||||
"2": [
|
||||
"vim::Number",
|
||||
2
|
||||
],
|
||||
"3": [
|
||||
"vim::Number",
|
||||
3
|
||||
],
|
||||
"4": [
|
||||
"vim::Number",
|
||||
4
|
||||
],
|
||||
"5": [
|
||||
"vim::Number",
|
||||
5
|
||||
],
|
||||
"6": [
|
||||
"vim::Number",
|
||||
6
|
||||
],
|
||||
"7": [
|
||||
"vim::Number",
|
||||
7
|
||||
],
|
||||
"8": [
|
||||
"vim::Number",
|
||||
8
|
||||
],
|
||||
"9": [
|
||||
"vim::Number",
|
||||
9
|
||||
],
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w left": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w right": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w up": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w down": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w h": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w l": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w k": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w j": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w ctrl-h": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w ctrl-l": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w ctrl-k": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w ctrl-j": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w shift-left": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w shift-right": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w shift-up": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w shift-down": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w shift-h": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w shift-l": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w shift-k": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w shift-j": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w g t": "pane::ActivateNextItem",
|
||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||
@@ -286,8 +379,14 @@
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
|
||||
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
|
||||
"ctrl-w n": [
|
||||
"workspace::NewFileInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w ctrl-n": [
|
||||
"workspace::NewFileInDirection",
|
||||
"Up"
|
||||
],
|
||||
"-": "pane::RevealInProjectPanel"
|
||||
}
|
||||
},
|
||||
@@ -303,12 +402,21 @@
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
".": "vim::Repeat",
|
||||
"c": ["vim::PushOperator", "Change"],
|
||||
"c": [
|
||||
"vim::PushOperator",
|
||||
"Change"
|
||||
],
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": ["vim::PushOperator", "Delete"],
|
||||
"d": [
|
||||
"vim::PushOperator",
|
||||
"Delete"
|
||||
],
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"y": ["vim::PushOperator", "Yank"],
|
||||
"y": [
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
@@ -339,7 +447,10 @@
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"> >": "editor::Indent",
|
||||
@@ -351,7 +462,10 @@
|
||||
{
|
||||
"context": "Editor && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
"0": [
|
||||
"vim::Number",
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -454,10 +568,22 @@
|
||||
"shift-i": "vim::InsertBefore",
|
||||
"shift-a": "vim::InsertAfter",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"ctrl-c": ["vim::SwitchMode", "Normal"],
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"],
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
],
|
||||
"ctrl-c": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
">": "editor::Indent",
|
||||
"<": "editor::Outdent",
|
||||
"i": [
|
||||
@@ -498,8 +624,14 @@
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"]
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -140,6 +140,14 @@
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
},
|
||||
"gutter": {
|
||||
// Whether to show line numbers in the gutter.
|
||||
"line_numbers": true,
|
||||
// Whether to show code action buttons in the gutter.
|
||||
"code_actions": true,
|
||||
// Whether to show fold buttons in the gutter.
|
||||
"folds": true
|
||||
},
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
"relative_line_numbers": false,
|
||||
@@ -331,7 +339,9 @@
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
// in any matching file.
|
||||
"disabled_globs": [".env"]
|
||||
"disabled_globs": [
|
||||
".env"
|
||||
]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@@ -440,7 +450,12 @@
|
||||
// Default directories to search for virtual environments, relative
|
||||
// to the current working directory. We recommend overriding this
|
||||
// in your project's settings, rather than globally.
|
||||
"directories": [".env", "env", ".venv", "venv"],
|
||||
"directories": [
|
||||
".env",
|
||||
"env",
|
||||
".venv",
|
||||
"venv"
|
||||
],
|
||||
// Can also be 'csh', 'fish', and `nushell`
|
||||
"activate_script": "default"
|
||||
}
|
||||
@@ -451,7 +466,10 @@
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Mono",
|
||||
// ---
|
||||
// Sets the maximum number of lines in the terminal's scrollback buffer.
|
||||
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
// "max_scroll_history_lines": 10000,
|
||||
},
|
||||
// Difference settings for semantic_index
|
||||
"semantic_index": {
|
||||
@@ -491,6 +509,9 @@
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true,
|
||||
@@ -499,6 +520,7 @@
|
||||
}
|
||||
},
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"JavaScript": {
|
||||
@@ -540,14 +562,18 @@
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
// "rust-analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// // These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
// "command": "clippy"
|
||||
// "check": {
|
||||
// "command": "clippy" // rust-analyzer.check.command (default: "check")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
// Vim settings
|
||||
"vim": {
|
||||
"use_system_clipboard": "always"
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
"server_url": "https://zed.dev",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Static runnables configuration.
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// "label": "human-readable label for UI",
|
||||
// "command": "bash",
|
||||
// // rest of the parameters are optional
|
||||
// "args": ["-c", "for i in {1..10}; do echo \"Second $i\"; sleep 1; done"],
|
||||
// // Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
// "env": {"foo": "bar"},
|
||||
// // Current working directory to spawn the command into, defaults to current project root.
|
||||
// "cwd": "/path/to/working/directory",
|
||||
// // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.
|
||||
// "use_new_terminal": false,
|
||||
// // Whether to allow multiple instances of the same runnable to be run, or rather wait for the existing ones to finish, defaults to `false`.
|
||||
// "allow_concurrent_runs": false,
|
||||
// },
|
||||
//
|
||||
{}
|
||||
19
assets/settings/initial_tasks.json
Normal file
19
assets/settings/initial_tasks.json
Normal file
@@ -0,0 +1,19 @@
|
||||
// Static tasks configuration.
|
||||
//
|
||||
// Example:
|
||||
[
|
||||
{
|
||||
"label": "Example task",
|
||||
"command": "bash",
|
||||
// rest of the parameters are optional
|
||||
"args": ["-c", "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done"],
|
||||
// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
"env": { "foo": "bar" },
|
||||
// Current working directory to spawn the command into, defaults to current project root.
|
||||
//"cwd": "/path/to/working/directory",
|
||||
// Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.
|
||||
"use_new_terminal": false,
|
||||
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
|
||||
"allow_concurrent_runs": false
|
||||
}
|
||||
]
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#21242bff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f7f7f84c",
|
||||
"scrollbar.thumb.background": "#f7f7f84c",
|
||||
"scrollbar.thumb.hover_background": "#252931ff",
|
||||
"scrollbar.thumb.border": "#252931ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#221f26ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#efecf44c",
|
||||
"scrollbar.thumb.background": "#efecf44c",
|
||||
"scrollbar.thumb.hover_background": "#332f38ff",
|
||||
"scrollbar.thumb.border": "#332f38ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -430,7 +430,7 @@
|
||||
"panel.background": "#e6e3ebff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#19171c4c",
|
||||
"scrollbar.thumb.background": "#19171c4c",
|
||||
"scrollbar.thumb.hover_background": "#cbc8d1ff",
|
||||
"scrollbar.thumb.border": "#cbc8d1ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -814,7 +814,7 @@
|
||||
"panel.background": "#262622ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fefbec4c",
|
||||
"scrollbar.thumb.background": "#fefbec4c",
|
||||
"scrollbar.thumb.hover_background": "#3b3933ff",
|
||||
"scrollbar.thumb.border": "#3b3933ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1198,7 +1198,7 @@
|
||||
"panel.background": "#eeebd7ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#20201d4c",
|
||||
"scrollbar.thumb.background": "#20201d4c",
|
||||
"scrollbar.thumb.hover_background": "#d7d3beff",
|
||||
"scrollbar.thumb.border": "#d7d3beff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1582,7 +1582,7 @@
|
||||
"panel.background": "#2c2b23ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f4f3ec4c",
|
||||
"scrollbar.thumb.background": "#f4f3ec4c",
|
||||
"scrollbar.thumb.hover_background": "#3c3b31ff",
|
||||
"scrollbar.thumb.border": "#3c3b31ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1966,7 +1966,7 @@
|
||||
"panel.background": "#ebeae3ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#22221b4c",
|
||||
"scrollbar.thumb.background": "#22221b4c",
|
||||
"scrollbar.thumb.hover_background": "#d0cfc5ff",
|
||||
"scrollbar.thumb.border": "#d0cfc5ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -2350,7 +2350,7 @@
|
||||
"panel.background": "#27211eff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f0eeed4c",
|
||||
"scrollbar.thumb.background": "#f0eeed4c",
|
||||
"scrollbar.thumb.hover_background": "#3b3431ff",
|
||||
"scrollbar.thumb.border": "#3b3431ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -2734,7 +2734,7 @@
|
||||
"panel.background": "#e9e6e4ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#1b19184c",
|
||||
"scrollbar.thumb.background": "#1b19184c",
|
||||
"scrollbar.thumb.hover_background": "#d6d1cfff",
|
||||
"scrollbar.thumb.border": "#d6d1cfff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -3118,7 +3118,7 @@
|
||||
"panel.background": "#252025ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f7f3f74c",
|
||||
"scrollbar.thumb.background": "#f7f3f74c",
|
||||
"scrollbar.thumb.hover_background": "#393239ff",
|
||||
"scrollbar.thumb.border": "#393239ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -3502,7 +3502,7 @@
|
||||
"panel.background": "#e0d5e0ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#1b181b4c",
|
||||
"scrollbar.thumb.background": "#1b181b4c",
|
||||
"scrollbar.thumb.hover_background": "#ccbdccff",
|
||||
"scrollbar.thumb.border": "#ccbdccff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -3886,7 +3886,7 @@
|
||||
"panel.background": "#1c2529ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#ebf8ff4c",
|
||||
"scrollbar.thumb.background": "#ebf8ff4c",
|
||||
"scrollbar.thumb.hover_background": "#2c3b42ff",
|
||||
"scrollbar.thumb.border": "#2c3b42ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -4270,7 +4270,7 @@
|
||||
"panel.background": "#cdeaf9ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#161b1d4c",
|
||||
"scrollbar.thumb.background": "#161b1d4c",
|
||||
"scrollbar.thumb.hover_background": "#b0d3e5ff",
|
||||
"scrollbar.thumb.border": "#b0d3e5ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -4654,7 +4654,7 @@
|
||||
"panel.background": "#252020ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f4ecec4c",
|
||||
"scrollbar.thumb.background": "#f4ecec4c",
|
||||
"scrollbar.thumb.hover_background": "#352f2fff",
|
||||
"scrollbar.thumb.border": "#352f2fff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -5038,7 +5038,7 @@
|
||||
"panel.background": "#ebe3e3ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#1b18184c",
|
||||
"scrollbar.thumb.background": "#1b18184c",
|
||||
"scrollbar.thumb.hover_background": "#cfc7c7ff",
|
||||
"scrollbar.thumb.border": "#cfc7c7ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -5422,7 +5422,7 @@
|
||||
"panel.background": "#1f2621ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#ecf4ee4c",
|
||||
"scrollbar.thumb.background": "#ecf4ee4c",
|
||||
"scrollbar.thumb.hover_background": "#2f3832ff",
|
||||
"scrollbar.thumb.border": "#2f3832ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -5806,7 +5806,7 @@
|
||||
"panel.background": "#e3ebe6ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#171c194c",
|
||||
"scrollbar.thumb.background": "#171c194c",
|
||||
"scrollbar.thumb.hover_background": "#c8d1cbff",
|
||||
"scrollbar.thumb.border": "#c8d1cbff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -6190,7 +6190,7 @@
|
||||
"panel.background": "#1f231fff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f3faf34c",
|
||||
"scrollbar.thumb.background": "#f3faf34c",
|
||||
"scrollbar.thumb.hover_background": "#333b33ff",
|
||||
"scrollbar.thumb.border": "#333b33ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -6574,7 +6574,7 @@
|
||||
"panel.background": "#daeedaff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#1315134c",
|
||||
"scrollbar.thumb.background": "#1315134c",
|
||||
"scrollbar.thumb.hover_background": "#bed7beff",
|
||||
"scrollbar.thumb.border": "#bed7beff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -6958,7 +6958,7 @@
|
||||
"panel.background": "#262f51ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f5f7ff4c",
|
||||
"scrollbar.thumb.background": "#f5f7ff4c",
|
||||
"scrollbar.thumb.hover_background": "#363f62ff",
|
||||
"scrollbar.thumb.border": "#363f62ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -7342,7 +7342,7 @@
|
||||
"panel.background": "#e5e8f5ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#2026464c",
|
||||
"scrollbar.thumb.background": "#2026464c",
|
||||
"scrollbar.thumb.hover_background": "#ccd0e1ff",
|
||||
"scrollbar.thumb.border": "#ccd0e1ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#1f2127ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#bfbdb64c",
|
||||
"scrollbar.thumb.background": "#bfbdb64c",
|
||||
"scrollbar.thumb.hover_background": "#2d2f34ff",
|
||||
"scrollbar.thumb.border": "#2d2f34ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -415,7 +415,7 @@
|
||||
"panel.background": "#ececedff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#5c61664c",
|
||||
"scrollbar.thumb.background": "#5c61664c",
|
||||
"scrollbar.thumb.hover_background": "#dfe0e1ff",
|
||||
"scrollbar.thumb.border": "#dfe0e1ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -784,7 +784,7 @@
|
||||
"panel.background": "#353944ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#cccac24c",
|
||||
"scrollbar.thumb.background": "#cccac24c",
|
||||
"scrollbar.thumb.hover_background": "#43464fff",
|
||||
"scrollbar.thumb.border": "#43464fff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#3a3735ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.hover_background": "#494340ff",
|
||||
"scrollbar.thumb.border": "#494340ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -420,7 +420,7 @@
|
||||
"panel.background": "#393634ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.hover_background": "#494340ff",
|
||||
"scrollbar.thumb.border": "#494340ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -794,7 +794,7 @@
|
||||
"panel.background": "#3b3735ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.hover_background": "#494340ff",
|
||||
"scrollbar.thumb.border": "#494340ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1168,7 +1168,7 @@
|
||||
"panel.background": "#ecddb4ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.hover_background": "#ddcca7ff",
|
||||
"scrollbar.thumb.border": "#ddcca7ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1542,7 +1542,7 @@
|
||||
"panel.background": "#ecddb5ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.hover_background": "#ddcca7ff",
|
||||
"scrollbar.thumb.border": "#ddcca7ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1916,7 +1916,7 @@
|
||||
"panel.background": "#ecdcb3ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.hover_background": "#ddcca7ff",
|
||||
"scrollbar.thumb.border": "#ddcca7ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#2f343eff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#c8ccd44c",
|
||||
"scrollbar.thumb.background": "#c8ccd44c",
|
||||
"scrollbar.thumb.hover_background": "#363c46ff",
|
||||
"scrollbar.thumb.border": "#363c46ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -420,7 +420,7 @@
|
||||
"panel.background": "#ebebecff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#383a414c",
|
||||
"scrollbar.thumb.background": "#383a414c",
|
||||
"scrollbar.thumb.hover_background": "#dfdfe0ff",
|
||||
"scrollbar.thumb.border": "#dfdfe0ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#1c1b2aff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.hover_background": "#232132ff",
|
||||
"scrollbar.thumb.border": "#232132ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -425,7 +425,7 @@
|
||||
"panel.background": "#fef9f2ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#5752794c",
|
||||
"scrollbar.thumb.background": "#5752794c",
|
||||
"scrollbar.thumb.hover_background": "#e5e0dfff",
|
||||
"scrollbar.thumb.border": "#e5e0dfff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -804,7 +804,7 @@
|
||||
"panel.background": "#28253cff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.hover_background": "#322f48ff",
|
||||
"scrollbar.thumb.border": "#322f48ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#2b3038ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fdf4c14c",
|
||||
"scrollbar.thumb.background": "#fdf4c14c",
|
||||
"scrollbar.thumb.hover_background": "#313741ff",
|
||||
"scrollbar.thumb.border": "#313741ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#04313bff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fdf6e34c",
|
||||
"scrollbar.thumb.background": "#fdf6e34c",
|
||||
"scrollbar.thumb.hover_background": "#053541ff",
|
||||
"scrollbar.thumb.border": "#053541ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -415,7 +415,7 @@
|
||||
"panel.background": "#f3eddaff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#002a354c",
|
||||
"scrollbar.thumb.background": "#002a354c",
|
||||
"scrollbar.thumb.hover_background": "#dcdacbff",
|
||||
"scrollbar.thumb.border": "#dcdacbff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#231f16ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f8f5de4c",
|
||||
"scrollbar.thumb.background": "#f8f5de4c",
|
||||
"scrollbar.thumb.hover_background": "#29251bff",
|
||||
"scrollbar.thumb.border": "#29251bff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -122,16 +122,13 @@ impl AssistantPanel {
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let (api_url, model_name) = cx
|
||||
.update(|cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
(
|
||||
settings.openai_api_url.clone(),
|
||||
settings.default_open_ai_model.full_name().to_string(),
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
.unwrap();
|
||||
let (api_url, model_name) = cx.update(|cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
(
|
||||
settings.openai_api_url.clone(),
|
||||
settings.default_open_ai_model.full_name().to_string(),
|
||||
)
|
||||
})?;
|
||||
let completion_provider = OpenAiCompletionProvider::new(
|
||||
api_url,
|
||||
model_name,
|
||||
@@ -365,7 +362,7 @@ impl AssistantPanel {
|
||||
move |cx: &mut BlockContext| {
|
||||
measurements.set(BlockMeasurements {
|
||||
anchor_x: cx.anchor_x,
|
||||
gutter_width: cx.gutter_width,
|
||||
gutter_width: cx.gutter_dimensions.width,
|
||||
});
|
||||
inline_assistant.clone().into_any_element()
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -4,12 +4,14 @@ use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, Task, ViewContext, VisualContext, WindowContext,
|
||||
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
|
||||
use markdown_preview::markdown_preview_view::MarkdownPreviewView;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
@@ -26,13 +28,24 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::http::{HttpClient, ZedHttpClient};
|
||||
use util::{
|
||||
http::{HttpClient, ZedHttpClient},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
ViewReleaseNotesLocally
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
@@ -96,6 +109,12 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
|
||||
|
||||
impl Global for GlobalAutoUpdate {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
@@ -105,6 +124,10 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
|
||||
workspace.register_action(|_, action, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -165,6 +188,71 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
|
||||
None
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.zed_url(&format!(
|
||||
"/api/release_notes/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
|
||||
.expect("creating buffers on a local workspace always succeeds");
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx
|
||||
.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
editor,
|
||||
workspace_handle,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item(Box::new(view.clone()), cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version;
|
||||
|
||||
@@ -156,7 +156,7 @@ impl Room {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
connect.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if !this.read_only() {
|
||||
if this.can_use_microphone() {
|
||||
if let Some(live_kit) = &this.live_kit {
|
||||
if !live_kit.muted_by_user && !live_kit.deafened {
|
||||
return this.share_microphone(cx);
|
||||
@@ -1322,11 +1322,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_only(&self) -> bool {
|
||||
!(self.local_participant().role == proto::ChannelRole::Member
|
||||
|| self.local_participant().role == proto::ChannelRole::Admin)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
@@ -1337,6 +1332,22 @@ impl Room {
|
||||
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
|
||||
}
|
||||
|
||||
pub fn can_use_microphone(&self) -> bool {
|
||||
use proto::ChannelRole::*;
|
||||
match self.local_participant.role {
|
||||
Admin | Member | Talker => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_share_projects(&self) -> bool {
|
||||
use proto::ChannelRole::*;
|
||||
match self.local_participant.role {
|
||||
Admin | Member => true,
|
||||
Guest | Banned | Talker => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
|
||||
@@ -120,7 +120,8 @@ impl ChannelMembership {
|
||||
proto::ChannelRole::Admin => 0,
|
||||
proto::ChannelRole::Member => 1,
|
||||
proto::ChannelRole::Banned => 2,
|
||||
proto::ChannelRole::Guest => 3,
|
||||
proto::ChannelRole::Talker => 3,
|
||||
proto::ChannelRole::Guest => 4,
|
||||
},
|
||||
kind_order: match self.kind {
|
||||
proto::channel_member::Kind::Member => 0,
|
||||
@@ -348,6 +349,21 @@ impl ChannelStore {
|
||||
.is_some_and(|state| state.has_new_messages())
|
||||
}
|
||||
|
||||
pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option<u64> {
|
||||
self.channel_states.get(&channel_id).and_then(|state| {
|
||||
if let Some(last_message_id) = state.latest_chat_message {
|
||||
if state
|
||||
.last_acknowledged_message_id()
|
||||
.is_some_and(|id| id < last_message_id)
|
||||
{
|
||||
return state.last_acknowledged_message_id();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn acknowledge_message_id(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
@@ -1152,6 +1168,10 @@ impl ChannelState {
|
||||
})
|
||||
}
|
||||
|
||||
fn last_acknowledged_message_id(&self) -> Option<u64> {
|
||||
self.observed_chat_message
|
||||
}
|
||||
|
||||
fn acknowledge_message_id(&mut self, message_id: u64) {
|
||||
let observed = self.observed_chat_message.get_or_insert(message_id);
|
||||
*observed = (*observed).max(message_id);
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||
|
||||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext, Context, Model, TestAppContext};
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
@@ -337,8 +338,9 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
release_channel::init("0.0.0", cx);
|
||||
client::init_settings(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
|
||||
client::init(&client, cx);
|
||||
|
||||
@@ -10,10 +10,11 @@ path = "src/client.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
gpui.workspace = true
|
||||
@@ -51,6 +52,7 @@ uuid.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -10,6 +10,7 @@ use async_tungstenite::tungstenite::{
|
||||
error::Error as WebsocketError,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
|
||||
@@ -421,11 +422,15 @@ impl settings::Settings for TelemetrySettings {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(http: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
http: Arc<ZedHttpClient>,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let client = Arc::new(Self {
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
telemetry: Telemetry::new(http.clone(), cx),
|
||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||
http,
|
||||
state: Default::default(),
|
||||
|
||||
@@ -1455,6 +1460,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test::FakeServer;
|
||||
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use settings::SettingsStore;
|
||||
@@ -1465,7 +1471,13 @@ mod tests {
|
||||
async fn test_reconnection(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
let mut status = client.status();
|
||||
assert!(matches!(
|
||||
@@ -1500,7 +1512,13 @@ mod tests {
|
||||
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut status = client.status();
|
||||
|
||||
// Time out when client tries to connect.
|
||||
@@ -1573,7 +1591,13 @@ mod tests {
|
||||
init_test(cx);
|
||||
let auth_count = Arc::new(Mutex::new(0));
|
||||
let dropped_auth_count = Arc::new(Mutex::new(0));
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
client.override_authenticate({
|
||||
let auth_count = auth_count.clone();
|
||||
let dropped_auth_count = dropped_auth_count.clone();
|
||||
@@ -1621,7 +1645,13 @@ mod tests {
|
||||
async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||
@@ -1675,7 +1705,13 @@ mod tests {
|
||||
async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
@@ -1704,7 +1740,13 @@ mod tests {
|
||||
async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
|
||||
@@ -2,6 +2,7 @@ mod event_coalescer;
|
||||
|
||||
use crate::TelemetrySettings;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -24,6 +25,7 @@ use util::TryFutureExt;
|
||||
use self::event_coalescer::EventCoalescer;
|
||||
|
||||
pub struct Telemetry {
|
||||
clock: Arc<dyn SystemClock>,
|
||||
http_client: Arc<ZedHttpClient>,
|
||||
executor: BackgroundExecutor,
|
||||
state: Arc<Mutex<TelemetryState>>,
|
||||
@@ -156,7 +158,11 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
client: Arc<ZedHttpClient>,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
|
||||
@@ -205,6 +211,7 @@ impl Telemetry {
|
||||
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
clock,
|
||||
http_client: client,
|
||||
executor: cx.background_executor().clone(),
|
||||
state,
|
||||
@@ -317,7 +324,8 @@ impl Telemetry {
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -333,7 +341,8 @@ impl Telemetry {
|
||||
suggestion_id,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -349,7 +358,8 @@ impl Telemetry {
|
||||
conversation_id,
|
||||
kind,
|
||||
model,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -365,7 +375,8 @@ impl Telemetry {
|
||||
operation,
|
||||
room_id,
|
||||
channel_id,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -375,7 +386,8 @@ impl Telemetry {
|
||||
let event = Event::Cpu {
|
||||
usage_as_percentage,
|
||||
core_count,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -389,24 +401,18 @@ impl Telemetry {
|
||||
let event = Event::Memory {
|
||||
memory_in_bytes,
|
||||
virtual_memory_in_bytes,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) {
|
||||
self.report_app_event_with_date_time(operation, Utc::now());
|
||||
}
|
||||
|
||||
fn report_app_event_with_date_time(
|
||||
self: &Arc<Self>,
|
||||
operation: String,
|
||||
date_time: DateTime<Utc>,
|
||||
) -> Event {
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
|
||||
let event = Event::App {
|
||||
operation,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(date_time),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event.clone());
|
||||
@@ -418,7 +424,8 @@ impl Telemetry {
|
||||
let event = Event::Setting {
|
||||
setting,
|
||||
value,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -433,7 +440,8 @@ impl Telemetry {
|
||||
let event = Event::Edit {
|
||||
duration: end.timestamp_millis() - start.timestamp_millis(),
|
||||
environment,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event);
|
||||
@@ -444,7 +452,8 @@ impl Telemetry {
|
||||
let event = Event::Action {
|
||||
source,
|
||||
action,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -590,29 +599,32 @@ impl Telemetry {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
let session_id = "session_id".to_string();
|
||||
|
||||
cx.update(|cx| {
|
||||
let telemetry = Telemetry::new(http, cx);
|
||||
let telemetry = Telemetry::new(clock.clone(), http, cx);
|
||||
|
||||
telemetry.state.lock().max_queue_size = 4;
|
||||
telemetry.start(installation_id, session_id, cx);
|
||||
|
||||
assert!(is_empty_state(&telemetry));
|
||||
|
||||
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
|
||||
let first_date_time = clock.utc_now();
|
||||
let operation = "test".to_string();
|
||||
|
||||
let event =
|
||||
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -627,9 +639,9 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
let mut date_time = first_date_time + chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -644,9 +656,9 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
date_time += chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -661,10 +673,10 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
date_time += chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
// Adding a 4th event should cause a flush
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -680,22 +692,24 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
let session_id = "session_id".to_string();
|
||||
|
||||
cx.update(|cx| {
|
||||
let telemetry = Telemetry::new(http, cx);
|
||||
let telemetry = Telemetry::new(clock.clone(), http, cx);
|
||||
telemetry.state.lock().max_queue_size = 4;
|
||||
telemetry.start(installation_id, session_id, cx);
|
||||
|
||||
assert!(is_empty_state(&telemetry));
|
||||
|
||||
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
|
||||
let first_date_time = clock.utc_now();
|
||||
let operation = "test".to_string();
|
||||
|
||||
let event =
|
||||
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
|
||||
@@ -9,5 +9,10 @@ license = "GPL-3.0-or-later"
|
||||
path = "src/clock.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["dep:parking_lot"]
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
parking_lot = { workspace = true, optional = true }
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
mod system_clock;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
fmt, iter,
|
||||
};
|
||||
|
||||
/// A unique identifier for each distributed node
|
||||
pub use system_clock::*;
|
||||
|
||||
/// A unique identifier for each distributed node.
|
||||
pub type ReplicaId = u16;
|
||||
|
||||
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp),
|
||||
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp).
|
||||
pub type Seq = u32;
|
||||
|
||||
/// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
|
||||
@@ -18,7 +22,7 @@ pub struct Lamport {
|
||||
pub value: Seq,
|
||||
}
|
||||
|
||||
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock)
|
||||
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
|
||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
||||
pub struct Global(SmallVec<[u32; 8]>);
|
||||
|
||||
|
||||
59
crates/clock/src/system_clock.rs
Normal file
59
crates/clock/src/system_clock.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
pub trait SystemClock: Send + Sync {
|
||||
/// Returns the current date and time in UTC.
|
||||
fn utc_now(&self) -> DateTime<Utc>;
|
||||
}
|
||||
|
||||
pub struct RealSystemClock;
|
||||
|
||||
impl SystemClock for RealSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
Utc::now()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeSystemClockState {
|
||||
now: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeSystemClock {
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
state: parking_lot::Mutex<FakeSystemClockState>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Default for FakeSystemClock {
|
||||
fn default() -> Self {
|
||||
Self::new(Utc::now())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeSystemClock {
|
||||
pub fn new(now: DateTime<Utc>) -> Self {
|
||||
let state = FakeSystemClockState { now };
|
||||
|
||||
Self {
|
||||
state: parking_lot::Mutex::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_now(&self, now: DateTime<Utc>) {
|
||||
self.state.lock().now = now;
|
||||
}
|
||||
|
||||
/// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration).
|
||||
pub fn advance(&self, duration: chrono::Duration) {
|
||||
self.state.lock().now += duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl SystemClock for FakeSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
self.state.lock().now
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ async fn get_extensions(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetExtensionsParams>,
|
||||
) -> Result<Json<GetExtensionsResponse>> {
|
||||
let extensions = app.db.get_extensions(params.filter.as_deref(), 30).await?;
|
||||
let extensions = app.db.get_extensions(params.filter.as_deref(), 500).await?;
|
||||
Ok(Json(GetExtensionsResponse { data: extensions }))
|
||||
}
|
||||
|
||||
|
||||
@@ -100,8 +100,12 @@ pub enum ChannelRole {
|
||||
#[sea_orm(string_value = "member")]
|
||||
#[default]
|
||||
Member,
|
||||
/// Talker can read, but not write.
|
||||
/// They can use microphones and the channel chat
|
||||
#[sea_orm(string_value = "talker")]
|
||||
Talker,
|
||||
/// Guest can read, but not write.
|
||||
/// (thought they can use the channel chat)
|
||||
/// They can not use microphones but can use the chat.
|
||||
#[sea_orm(string_value = "guest")]
|
||||
Guest,
|
||||
/// Banned may not read.
|
||||
@@ -114,8 +118,9 @@ impl ChannelRole {
|
||||
pub fn should_override(&self, other: Self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin => matches!(other, Member | Banned | Guest),
|
||||
Member => matches!(other, Banned | Guest),
|
||||
Admin => matches!(other, Member | Banned | Talker | Guest),
|
||||
Member => matches!(other, Banned | Talker | Guest),
|
||||
Talker => matches!(other, Guest),
|
||||
Banned => matches!(other, Guest),
|
||||
Guest => false,
|
||||
}
|
||||
@@ -134,7 +139,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest => visibility == ChannelVisibility::Public,
|
||||
Guest | Talker => visibility == ChannelVisibility::Public,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -144,7 +149,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
Guest | Talker | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,16 +157,16 @@ impl ChannelRole {
|
||||
pub fn can_only_see_public_descendants(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Guest => true,
|
||||
Guest | Talker => true,
|
||||
Admin | Member | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the role can share screen/microphone/projects into rooms.
|
||||
pub fn can_publish_to_rooms(&self) -> bool {
|
||||
pub fn can_use_microphone(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Admin | Member | Talker => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -171,7 +176,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
Talker | Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +184,7 @@ impl ChannelRole {
|
||||
pub fn can_read_projects(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member | Guest => true,
|
||||
Admin | Member | Guest | Talker => true,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -188,7 +193,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Banned | Guest => false,
|
||||
Banned | Guest | Talker => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,6 +203,7 @@ impl From<proto::ChannelRole> for ChannelRole {
|
||||
match value {
|
||||
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||
proto::ChannelRole::Member => ChannelRole::Member,
|
||||
proto::ChannelRole::Talker => ChannelRole::Talker,
|
||||
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||
}
|
||||
@@ -209,6 +215,7 @@ impl Into<proto::ChannelRole> for ChannelRole {
|
||||
match self {
|
||||
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||
ChannelRole::Member => proto::ChannelRole::Member,
|
||||
ChannelRole::Talker => proto::ChannelRole::Talker,
|
||||
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||
}
|
||||
|
||||
@@ -795,6 +795,7 @@ impl Database {
|
||||
match role {
|
||||
Some(ChannelRole::Admin) => Ok(role.unwrap()),
|
||||
Some(ChannelRole::Member)
|
||||
| Some(ChannelRole::Talker)
|
||||
| Some(ChannelRole::Banned)
|
||||
| Some(ChannelRole::Guest)
|
||||
| None => Err(anyhow!(
|
||||
@@ -813,7 +814,10 @@ impl Database {
|
||||
let channel_role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||
match channel_role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
|
||||
Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
|
||||
Some(ChannelRole::Banned)
|
||||
| Some(ChannelRole::Guest)
|
||||
| Some(ChannelRole::Talker)
|
||||
| None => Err(anyhow!(
|
||||
"user is not a channel member or channel does not exist"
|
||||
))?,
|
||||
}
|
||||
@@ -828,9 +832,10 @@ impl Database {
|
||||
) -> Result<ChannelRole> {
|
||||
let role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||
match role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
|
||||
Ok(role.unwrap())
|
||||
}
|
||||
Some(ChannelRole::Admin)
|
||||
| Some(ChannelRole::Member)
|
||||
| Some(ChannelRole::Guest)
|
||||
| Some(ChannelRole::Talker) => Ok(role.unwrap()),
|
||||
Some(ChannelRole::Banned) | None => Err(anyhow!(
|
||||
"user is not a channel participant or channel does not exist"
|
||||
))?,
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Database {
|
||||
if !participant
|
||||
.role
|
||||
.unwrap_or(ChannelRole::Member)
|
||||
.can_publish_to_rooms()
|
||||
.can_edit_projects()
|
||||
{
|
||||
return Err(anyhow!("guests cannot share projects"))?;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl Database {
|
||||
|
||||
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
|
||||
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
|
||||
ChannelRole::Guest => ChannelRole::Guest,
|
||||
ChannelRole::Guest | ChannelRole::Talker => ChannelRole::Guest,
|
||||
ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()),
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ use axum::{
|
||||
Extension, Router, TypedHeader,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use connection_pool::ConnectionPool;
|
||||
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::{self, BoxFuture},
|
||||
@@ -558,6 +558,7 @@ impl Server {
|
||||
connection: Connection,
|
||||
address: String,
|
||||
user: User,
|
||||
zed_version: ZedVersion,
|
||||
impersonator: Option<User>,
|
||||
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
executor: Executor,
|
||||
@@ -599,7 +600,7 @@ impl Server {
|
||||
|
||||
{
|
||||
let mut pool = this.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user_id, user.admin);
|
||||
pool.add_connection(connection_id, user_id, user.admin, zed_version);
|
||||
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
||||
this.peer.send(connection_id, build_update_user_channels(&channels_for_user))?;
|
||||
this.peer.send(connection_id, build_channels_update(
|
||||
@@ -879,17 +880,20 @@ pub async fn handle_websocket_request(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// the first version of zed that sent this header was 0.121.x
|
||||
if let Some(version) = app_version_header.map(|header| header.0 .0) {
|
||||
// 0.123.0 was a nightly version with incompatible collab changes
|
||||
// that were reverted.
|
||||
if version == "0.123.0".parse().unwrap() {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"client must be upgraded".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let Some(version) = app_version_header.map(|header| ZedVersion(header.0 .0)) else {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"no version header found".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
|
||||
if !version.is_supported() {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"client must be upgraded".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let socket_address = socket_address.to_string();
|
||||
@@ -906,6 +910,7 @@ pub async fn handle_websocket_request(
|
||||
connection,
|
||||
socket_address,
|
||||
user,
|
||||
version,
|
||||
impersonator.0,
|
||||
None,
|
||||
Executor::Production,
|
||||
@@ -1311,6 +1316,22 @@ async fn set_room_participant_role(
|
||||
response: Response<proto::SetRoomParticipantRole>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let user_id = UserId::from_proto(request.user_id);
|
||||
let role = ChannelRole::from(request.role());
|
||||
|
||||
if role == ChannelRole::Talker {
|
||||
let pool = session.connection_pool().await;
|
||||
|
||||
for connection in pool.user_connections(user_id) {
|
||||
if !connection.zed_version.supports_talker_role() {
|
||||
Err(anyhow!(
|
||||
"This user is on zed {} which does not support unmute",
|
||||
connection.zed_version
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (live_kit_room, can_publish) = {
|
||||
let room = session
|
||||
.db()
|
||||
@@ -1318,13 +1339,13 @@ async fn set_room_participant_role(
|
||||
.set_room_participant_role(
|
||||
session.user_id,
|
||||
RoomId::from_proto(request.room_id),
|
||||
UserId::from_proto(request.user_id),
|
||||
ChannelRole::from(request.role()),
|
||||
user_id,
|
||||
role,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_room = room.live_kit_room.clone();
|
||||
let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms();
|
||||
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
|
||||
room_updated(&room, &session.peer);
|
||||
(live_kit_room, can_publish)
|
||||
};
|
||||
@@ -2088,21 +2109,16 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
|
||||
};
|
||||
|
||||
// For now, don't send view update messages back to that view's current leader.
|
||||
let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
|
||||
let peer_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
|
||||
_ => None,
|
||||
});
|
||||
|
||||
for follower_peer_id in request.follower_ids.iter().copied() {
|
||||
let follower_connection_id = follower_peer_id.into();
|
||||
if Some(follower_peer_id) != connection_id_to_omit
|
||||
&& connection_ids.contains(&follower_connection_id)
|
||||
{
|
||||
session.peer.forward_send(
|
||||
session.connection_id,
|
||||
follower_connection_id,
|
||||
request.clone(),
|
||||
)?;
|
||||
for connection_id in connection_ids.iter().cloned() {
|
||||
if Some(connection_id.into()) != peer_id_to_omit && connection_id != session.connection_id {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, request.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -4,6 +4,7 @@ use collections::{BTreeMap, HashSet};
|
||||
use rpc::ConnectionId;
|
||||
use serde::Serialize;
|
||||
use tracing::instrument;
|
||||
use util::SemanticVersion;
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct ConnectionPool {
|
||||
@@ -16,10 +17,30 @@ struct ConnectedUser {
|
||||
connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ZedVersion(pub SemanticVersion);
|
||||
use std::fmt;
|
||||
|
||||
impl fmt::Display for ZedVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ZedVersion {
|
||||
pub fn is_supported(&self) -> bool {
|
||||
self.0 != SemanticVersion::new(0, 123, 0)
|
||||
}
|
||||
pub fn supports_talker_role(&self) -> bool {
|
||||
self.0 >= SemanticVersion::new(0, 125, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Connection {
|
||||
pub user_id: UserId,
|
||||
pub admin: bool,
|
||||
pub zed_version: ZedVersion,
|
||||
}
|
||||
|
||||
impl ConnectionPool {
|
||||
@@ -29,9 +50,21 @@ impl ConnectionPool {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
|
||||
self.connections
|
||||
.insert(connection_id, Connection { user_id, admin });
|
||||
pub fn add_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
zed_version: ZedVersion,
|
||||
) {
|
||||
self.connections.insert(
|
||||
connection_id,
|
||||
Connection {
|
||||
user_id,
|
||||
admin,
|
||||
zed_version,
|
||||
},
|
||||
);
|
||||
let connected_user = self.connected_users.entry(user_id).or_default();
|
||||
connected_user.connection_ids.insert(connection_id);
|
||||
}
|
||||
@@ -57,6 +90,19 @@ impl ConnectionPool {
|
||||
self.connections.values()
|
||||
}
|
||||
|
||||
pub fn user_connections(&self, user_id: UserId) -> impl Iterator<Item = &Connection> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
.into_iter()
|
||||
.map(|state| {
|
||||
state
|
||||
.connection_ids
|
||||
.iter()
|
||||
.flat_map(|cid| self.connections.get(cid))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
|
||||
@@ -104,7 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
});
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
assert!(room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
@@ -130,7 +130,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||
|
||||
// B sees themselves as muted, and can unmute.
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
|
||||
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
|
||||
cx_a.run_until_parked();
|
||||
@@ -223,7 +223,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
let room_b = cx_b
|
||||
.read(ActiveCall::global)
|
||||
.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
@@ -240,7 +240,26 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.await
|
||||
.unwrap_err();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Talker,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
|
||||
// User B signs the zed CLA.
|
||||
server
|
||||
@@ -264,5 +283,6 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
db::{tests::TestDb, NewUserParams, UserId},
|
||||
executor::Executor,
|
||||
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
rpc::{Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
AppState, Config,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -10,6 +10,7 @@ use channel::{ChannelBuffer, ChannelStore};
|
||||
use client::{
|
||||
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||
};
|
||||
use clock::FakeSystemClock;
|
||||
use collab_ui::channel_view::ChannelView;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
@@ -37,7 +38,7 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::http::FakeHttpClient;
|
||||
use util::{http::FakeHttpClient, SemanticVersion};
|
||||
use workspace::{Workspace, WorkspaceStore};
|
||||
|
||||
pub struct TestServer {
|
||||
@@ -163,6 +164,7 @@ impl TestServer {
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
||||
{
|
||||
@@ -185,7 +187,7 @@ impl TestServer {
|
||||
.user_id
|
||||
};
|
||||
let client_name = name.to_string();
|
||||
let mut client = cx.update(|cx| Client::new(http.clone(), cx));
|
||||
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
|
||||
let server = self.server.clone();
|
||||
let db = self.app_state.db.clone();
|
||||
let connection_killers = self.connection_killers.clone();
|
||||
@@ -231,6 +233,7 @@ impl TestServer {
|
||||
server_conn,
|
||||
client_name,
|
||||
user,
|
||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
@@ -273,7 +276,7 @@ impl TestServer {
|
||||
collab_ui::init(&app_state, cx);
|
||||
file_finder::init(cx);
|
||||
menu::init();
|
||||
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
|
||||
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
|
||||
});
|
||||
|
||||
client
|
||||
|
||||
@@ -34,6 +34,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extensions_ui.workspace = true
|
||||
feature_flags.workspace = true
|
||||
feedback.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -63,6 +63,7 @@ pub struct ChatPanel {
|
||||
focus_handle: FocusHandle,
|
||||
open_context_menu: Option<(u64, Subscription)>,
|
||||
highlighted_message: Option<(u64, Task<()>)>,
|
||||
last_acknowledged_message_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -126,6 +127,7 @@ impl ChatPanel {
|
||||
focus_handle: cx.focus_handle(),
|
||||
open_context_menu: None,
|
||||
highlighted_message: None,
|
||||
last_acknowledged_message_id: None,
|
||||
};
|
||||
|
||||
if let Some(channel_id) = ActiveCall::global(cx)
|
||||
@@ -281,6 +283,13 @@ impl ChatPanel {
|
||||
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.active && self.is_scrolled_to_bottom {
|
||||
if let Some((chat, _)) = &self.active_chat {
|
||||
if let Some(channel_id) = self.channel_id(cx) {
|
||||
self.last_acknowledged_message_id = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.last_acknowledge_message_id(channel_id);
|
||||
}
|
||||
|
||||
chat.update(cx, |chat, cx| {
|
||||
chat.acknowledge_last_message(cx);
|
||||
});
|
||||
@@ -362,9 +371,9 @@ impl ChatPanel {
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.mb_1()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.overflow_hidden()
|
||||
.max_h_12()
|
||||
.child(reply_to_message_body.element(body_element_id, cx)),
|
||||
),
|
||||
@@ -454,120 +463,145 @@ impl ChatPanel {
|
||||
cx.theme().colors().panel_background
|
||||
};
|
||||
|
||||
v_flex().w_full().relative().child(
|
||||
div()
|
||||
.bg(background)
|
||||
.rounded_md()
|
||||
.overflow_hidden()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.when(!is_continuation_from_previous, |this| {
|
||||
this.mt_2().child(
|
||||
h_flex()
|
||||
.text_ui_sm()
|
||||
.child(div().absolute().child(
|
||||
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())),
|
||||
)
|
||||
.child(
|
||||
Label::new(format_timestamp(
|
||||
OffsetDateTime::now_utc(),
|
||||
message.timestamp,
|
||||
self.local_timezone,
|
||||
None,
|
||||
v_flex()
|
||||
.w_full()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.bg(background)
|
||||
.rounded_md()
|
||||
.overflow_hidden()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.when(!is_continuation_from_previous, |this| {
|
||||
this.mt_2().child(
|
||||
h_flex()
|
||||
.text_ui_sm()
|
||||
.child(div().absolute().child(
|
||||
Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
.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())),
|
||||
)
|
||||
.child(
|
||||
Label::new(format_timestamp(
|
||||
OffsetDateTime::now_utc(),
|
||||
message.timestamp,
|
||||
self.local_timezone,
|
||||
None,
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
message.reply_to_message_id.is_some() && reply_to_message.is_none(),
|
||||
|this| {
|
||||
const MESSAGE_DELETED: &str = "Message has been deleted";
|
||||
|
||||
let body_text = StyledText::new(MESSAGE_DELETED).with_highlights(
|
||||
&cx.text_style(),
|
||||
vec![(
|
||||
0..MESSAGE_DELETED.len(),
|
||||
HighlightStyle {
|
||||
font_style: Some(FontStyle::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
);
|
||||
|
||||
this.child(
|
||||
div()
|
||||
.border_l_2()
|
||||
.text_ui_xs()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.child(body_text),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.when(
|
||||
message.reply_to_message_id.is_some() && reply_to_message.is_none(),
|
||||
|this| {
|
||||
const MESSAGE_DELETED: &str = "Message has been deleted";
|
||||
|
||||
let body_text = StyledText::new(MESSAGE_DELETED).with_highlights(
|
||||
&cx.text_style(),
|
||||
vec![(
|
||||
0..MESSAGE_DELETED.len(),
|
||||
HighlightStyle {
|
||||
font_style: Some(FontStyle::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
);
|
||||
|
||||
this.child(
|
||||
div()
|
||||
.border_l_2()
|
||||
.text_ui_xs()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.child(body_text),
|
||||
.when_some(reply_to_message, |el, reply_to_message| {
|
||||
el.child(self.render_replied_to_message(
|
||||
Some(message.id),
|
||||
&reply_to_message,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.when(mentioning_you || replied_to_you, |this| this.my_0p5())
|
||||
.map(|el| {
|
||||
let text = self.markdown_data.entry(message.id).or_insert_with(|| {
|
||||
Self::render_markdown_with_mentions(
|
||||
&self.languages,
|
||||
self.client.id(),
|
||||
&message,
|
||||
)
|
||||
});
|
||||
el.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.text_ui_sm()
|
||||
.id(element_id)
|
||||
.group("")
|
||||
.child(text.element("body".into(), cx))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.z_index(1)
|
||||
.right_0()
|
||||
.w_6()
|
||||
.bg(background)
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
el.visible_on_hover("")
|
||||
})
|
||||
.when_some(message_id, |el, message_id| {
|
||||
el.child(
|
||||
popover_menu(("menu", message_id))
|
||||
.trigger(IconButton::new(
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
))
|
||||
.menu(move |cx| {
|
||||
Some(Self::render_message_menu(
|
||||
&this,
|
||||
message_id,
|
||||
can_delete_message,
|
||||
cx,
|
||||
))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when_some(reply_to_message, |el, reply_to_message| {
|
||||
el.child(self.render_replied_to_message(
|
||||
Some(message.id),
|
||||
&reply_to_message,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.when(mentioning_you || replied_to_you, |this| this.my_0p5())
|
||||
.map(|el| {
|
||||
let text = self.markdown_data.entry(message.id).or_insert_with(|| {
|
||||
Self::render_markdown_with_mentions(
|
||||
&self.languages,
|
||||
self.client.id(),
|
||||
&message,
|
||||
)
|
||||
});
|
||||
el.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.text_ui_sm()
|
||||
.id(element_id)
|
||||
.group("")
|
||||
.child(text.element("body".into(), cx))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
self.last_acknowledged_message_id
|
||||
.is_some_and(|l| Some(l) == message_id),
|
||||
|this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.py_2()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(div().w_full().h_0p5().bg(cx.theme().colors().border))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.z_index(1)
|
||||
.right_0()
|
||||
.w_6()
|
||||
.bg(background)
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
el.visible_on_hover("")
|
||||
})
|
||||
.when_some(message_id, |el, message_id| {
|
||||
el.child(
|
||||
popover_menu(("menu", message_id))
|
||||
.trigger(IconButton::new(
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
))
|
||||
.menu(move |cx| {
|
||||
Some(Self::render_message_menu(
|
||||
&this,
|
||||
message_id,
|
||||
can_delete_message,
|
||||
cx,
|
||||
))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
.px_1()
|
||||
.rounded_md()
|
||||
.text_ui_xs()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child("New messages"),
|
||||
)
|
||||
.child(div().w_full().h_0p5().bg(cx.theme().colors().border)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn has_open_menu(&self, message_id: Option<u64>) -> bool {
|
||||
@@ -682,8 +716,11 @@ impl ChatPanel {
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let chat = open_chat.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let highlight_message_id = scroll_to_message_id;
|
||||
let scroll_to_message_id = this.update(&mut cx, |this, cx| {
|
||||
this.set_active_chat(chat.clone(), cx);
|
||||
|
||||
scroll_to_message_id.or_else(|| this.last_acknowledged_message_id)
|
||||
})?;
|
||||
|
||||
if let Some(message_id) = scroll_to_message_id {
|
||||
@@ -691,21 +728,22 @@ impl ChatPanel {
|
||||
ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
|
||||
.await
|
||||
{
|
||||
let task = cx.spawn({
|
||||
let this = this.clone();
|
||||
|
||||
|mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_message.take();
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_message = Some((message_id, task));
|
||||
if let Some(highlight_message_id) = highlight_message_id {
|
||||
let task = cx.spawn({
|
||||
|this, mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_message.take();
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
this.highlighted_message = Some((highlight_message_id, task));
|
||||
}
|
||||
|
||||
if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
|
||||
this.message_list.scroll_to(ListOffset {
|
||||
item_ix,
|
||||
@@ -802,18 +840,21 @@ impl Render for ChatPanel {
|
||||
|
||||
el.when_some(reply_message, |el, reply_message| {
|
||||
el.child(
|
||||
div()
|
||||
h_flex()
|
||||
.when(!self.is_scrolled_to_bottom, |el| {
|
||||
el.border_t_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.flex()
|
||||
.w_full()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.items_start()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(self.render_replied_to_message(None, &reply_message, cx))
|
||||
.child(
|
||||
div().flex_shrink().overflow_hidden().child(
|
||||
self.render_replied_to_message(None, &reply_message, cx),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("close-reply-preview", IconName::Close)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
@@ -1056,6 +1097,107 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_render_markdown_with_auto_detect_links() {
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
let message = channel::ChannelMessage {
|
||||
id: ChannelMessageId::Saved(0),
|
||||
body: "Here is a link https://zed.dev to zeds website".to_string(),
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
sender: Arc::new(client::User {
|
||||
github_login: "fgh".into(),
|
||||
avatar_uri: "avatar_fgh".into(),
|
||||
id: 103,
|
||||
}),
|
||||
nonce: 5,
|
||||
mentions: Vec::new(),
|
||||
reply_to_message_id: None,
|
||||
};
|
||||
|
||||
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||
|
||||
// Note that the "'" was replaced with ’ due to smart punctuation.
|
||||
let (body, ranges) =
|
||||
marked_text_ranges("Here is a link «https://zed.dev» to zeds website", false);
|
||||
assert_eq!(message.text, body);
|
||||
assert_eq!(1, ranges.len());
|
||||
assert_eq!(
|
||||
message.highlights,
|
||||
vec![(
|
||||
ranges[0].clone(),
|
||||
HighlightStyle {
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
),]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_render_markdown_with_auto_detect_links_and_additional_formatting() {
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
let message = channel::ChannelMessage {
|
||||
id: ChannelMessageId::Saved(0),
|
||||
body: "**Here is a link https://zed.dev to zeds website**".to_string(),
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
sender: Arc::new(client::User {
|
||||
github_login: "fgh".into(),
|
||||
avatar_uri: "avatar_fgh".into(),
|
||||
id: 103,
|
||||
}),
|
||||
nonce: 5,
|
||||
mentions: Vec::new(),
|
||||
reply_to_message_id: None,
|
||||
};
|
||||
|
||||
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||
|
||||
// Note that the "'" was replaced with ’ due to smart punctuation.
|
||||
let (body, ranges) = marked_text_ranges(
|
||||
"«Here is a link »«https://zed.dev»« to zeds website»",
|
||||
false,
|
||||
);
|
||||
assert_eq!(message.text, body);
|
||||
assert_eq!(3, ranges.len());
|
||||
assert_eq!(
|
||||
message.highlights,
|
||||
vec![
|
||||
(
|
||||
ranges[0].clone(),
|
||||
HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
),
|
||||
(
|
||||
ranges[1].clone(),
|
||||
HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
),
|
||||
(
|
||||
ranges[2].clone(),
|
||||
HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_locale() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0);
|
||||
|
||||
@@ -385,6 +385,7 @@ impl Render for MessageEditor {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use language::{Language, LanguageConfig};
|
||||
use rpc::proto;
|
||||
@@ -455,8 +456,9 @@ mod tests {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
|
||||
@@ -34,7 +34,7 @@ use std::{mem, sync::Arc};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu,
|
||||
Icon, IconButton, IconName, IconSize, Label, ListHeader, ListItem, Tooltip,
|
||||
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
@@ -854,6 +854,10 @@ impl CollabPanel {
|
||||
.into_any_element()
|
||||
} else if role == proto::ChannelRole::Guest {
|
||||
Label::new("Guest").color(Color::Muted).into_any_element()
|
||||
} else if role == proto::ChannelRole::Talker {
|
||||
Label::new("Mic only")
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
})
|
||||
@@ -959,6 +963,8 @@ impl CollabPanel {
|
||||
is_selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
|
||||
ListItem::new("channel-notes")
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
@@ -966,9 +972,19 @@ impl CollabPanel {
|
||||
}))
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, true, cx))
|
||||
.child(IconButton::new(0, IconName::File)),
|
||||
.child(IconButton::new(0, IconName::File))
|
||||
.children(has_channel_buffer_changed.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(2.))
|
||||
.top(px(2.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(Label::new("notes"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||
@@ -980,6 +996,8 @@ impl CollabPanel {
|
||||
is_selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let has_messages_notification = channel_store.has_new_messages(channel_id);
|
||||
ListItem::new("channel-chat")
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
@@ -987,9 +1005,19 @@ impl CollabPanel {
|
||||
}))
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, false, cx))
|
||||
.child(IconButton::new(0, IconName::MessageBubbles)),
|
||||
.child(IconButton::new(0, IconName::MessageBubbles))
|
||||
.children(has_messages_notification.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(2.))
|
||||
.top(px(4.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(Label::new("chat"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||
@@ -1013,13 +1041,38 @@ impl CollabPanel {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let this = cx.view().clone();
|
||||
if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) {
|
||||
if !(role == proto::ChannelRole::Guest
|
||||
|| role == proto::ChannelRole::Talker
|
||||
|| role == proto::ChannelRole::Member)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
|
||||
let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
|
||||
if role == proto::ChannelRole::Guest {
|
||||
context_menu.entry(
|
||||
context_menu = context_menu.entry(
|
||||
"Grant Mic Access",
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| {
|
||||
let Some(room) = call.room() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
room.update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
user_id,
|
||||
proto::ChannelRole::Talker,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to grant mic access", cx, |_, _| None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
|
||||
context_menu = context_menu.entry(
|
||||
"Grant Write Access",
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
@@ -1043,10 +1096,16 @@ impl CollabPanel {
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
} else if role == proto::ChannelRole::Member {
|
||||
context_menu.entry(
|
||||
"Revoke Write Access",
|
||||
);
|
||||
}
|
||||
if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
|
||||
let label = if role == proto::ChannelRole::Talker {
|
||||
"Mute"
|
||||
} else {
|
||||
"Revoke Access"
|
||||
};
|
||||
context_menu = context_menu.entry(
|
||||
label,
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
@@ -1062,12 +1121,12 @@ impl CollabPanel {
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
|
||||
.detach_and_prompt_err("Failed to revoke access", cx, |_, _| None)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
unreachable!()
|
||||
);
|
||||
}
|
||||
|
||||
context_menu
|
||||
});
|
||||
|
||||
cx.focus_view(&context_menu);
|
||||
@@ -2490,13 +2549,26 @@ impl CollabPanel {
|
||||
},
|
||||
))
|
||||
.start_slot(
|
||||
Icon::new(if is_public {
|
||||
IconName::Public
|
||||
} else {
|
||||
IconName::Hash
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
div()
|
||||
.relative()
|
||||
.child(
|
||||
Icon::new(if is_public {
|
||||
IconName::Public
|
||||
} else {
|
||||
IconName::Hash
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(has_notes_notification.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(-1.))
|
||||
.top(px(-1.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -2530,9 +2602,7 @@ impl CollabPanel {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
|
||||
.when(!has_messages_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
}),
|
||||
.visible_on_hover(""),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("channel_notes", IconName::File)
|
||||
@@ -2548,9 +2618,7 @@ impl CollabPanel {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
|
||||
.when(!has_notes_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
}),
|
||||
.visible_on_hover(""),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -2560,6 +2628,7 @@ impl CollabPanel {
|
||||
cx.new_view(|_| JoinChannelTooltip {
|
||||
channel_store: channel_store.clone(),
|
||||
channel_id,
|
||||
has_notes_notification,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
@@ -2603,24 +2672,32 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) ->
|
||||
let right = bounds.right();
|
||||
let top = bounds.top();
|
||||
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last {
|
||||
start_y
|
||||
} else {
|
||||
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
|
||||
},
|
||||
cx.paint_quad(
|
||||
fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last {
|
||||
start_y
|
||||
} else {
|
||||
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
|
||||
},
|
||||
),
|
||||
),
|
||||
color,
|
||||
),
|
||||
color,
|
||||
));
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
color,
|
||||
));
|
||||
None,
|
||||
None,
|
||||
);
|
||||
cx.paint_quad(
|
||||
fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
color,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
})
|
||||
.w(width)
|
||||
.h(line_height)
|
||||
@@ -2845,17 +2922,25 @@ impl Render for DraggedChannelView {
|
||||
struct JoinChannelTooltip {
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_notes_notification: bool,
|
||||
}
|
||||
|
||||
impl Render for JoinChannelTooltip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
tooltip_container(cx, |div, cx| {
|
||||
tooltip_container(cx, |container, cx| {
|
||||
let participants = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_participants(self.channel_id);
|
||||
|
||||
div.child(Label::new("Join Channel"))
|
||||
container
|
||||
.child(Label::new("Join channel"))
|
||||
.children(self.has_notes_notification.then(|| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
.child(Label::new("Unread notes"))
|
||||
}))
|
||||
.children(participants.iter().map(|participant| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -187,9 +187,10 @@ impl Render for CollabTitlebarItem {
|
||||
let is_muted = room.is_muted();
|
||||
let is_deafened = room.is_deafened().unwrap_or(false);
|
||||
let is_screen_sharing = room.is_screen_sharing();
|
||||
let read_only = room.read_only();
|
||||
let can_use_microphone = room.can_use_microphone();
|
||||
let can_share_projects = room.can_share_projects();
|
||||
|
||||
this.when(is_local && !read_only, |this| {
|
||||
this.when(is_local && can_share_projects, |this| {
|
||||
this.child(
|
||||
Button::new(
|
||||
"toggle_sharing",
|
||||
@@ -235,7 +236,7 @@ impl Render for CollabTitlebarItem {
|
||||
)
|
||||
.pr_2(),
|
||||
)
|
||||
.when(!read_only, |this| {
|
||||
.when(can_use_microphone, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"mute-microphone",
|
||||
@@ -276,7 +277,7 @@ impl Render for CollabTitlebarItem {
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_deafened)
|
||||
.tooltip(move |cx| {
|
||||
if !read_only {
|
||||
if can_use_microphone {
|
||||
Tooltip::with_meta(
|
||||
"Deafen Audio",
|
||||
None,
|
||||
@@ -289,7 +290,7 @@ impl Render for CollabTitlebarItem {
|
||||
})
|
||||
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
|
||||
)
|
||||
.when(!read_only, |this| {
|
||||
.when(can_share_projects, |this| {
|
||||
this.child(
|
||||
IconButton::new("screen-share", ui::IconName::Screen)
|
||||
.style(ButtonStyle::Subtle)
|
||||
@@ -421,14 +422,20 @@ impl CollabTitlebarItem {
|
||||
worktree.root_name()
|
||||
});
|
||||
|
||||
names.next().unwrap_or("")
|
||||
names.next()
|
||||
};
|
||||
let is_project_selected = name.is_some();
|
||||
let name = if let Some(name) = name {
|
||||
util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
|
||||
} else {
|
||||
"Open recent project".to_string()
|
||||
};
|
||||
|
||||
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
|
||||
let workspace = self.workspace.clone();
|
||||
popover_menu("project_name_trigger")
|
||||
.trigger(
|
||||
Button::new("project_name_trigger", name)
|
||||
.when(!is_project_selected, |b| b.color(Color::Muted))
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
||||
@@ -689,6 +696,7 @@ impl CollabTitlebarItem {
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
@@ -714,6 +722,7 @@ impl CollabTitlebarItem {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use client::telemetry::Telemetry;
|
||||
@@ -9,11 +10,12 @@ use copilot::CommandPaletteFilter;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
use release_channel::{parse_zed_link, ReleaseChannel};
|
||||
use postage::{sink::Sink, stream::Stream};
|
||||
use release_channel::parse_zed_link;
|
||||
use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace};
|
||||
@@ -119,6 +121,10 @@ pub struct CommandPaletteDelegate {
|
||||
selected_ix: usize,
|
||||
telemetry: Arc<Telemetry>,
|
||||
previous_focus_handle: FocusHandle,
|
||||
updating_matches: Option<(
|
||||
Task<()>,
|
||||
postage::dispatch::Receiver<(Vec<Command>, Vec<StringMatch>)>,
|
||||
)>,
|
||||
}
|
||||
|
||||
struct Command {
|
||||
@@ -138,7 +144,7 @@ impl Clone for Command {
|
||||
/// Hit count for each command in the palette.
|
||||
/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
|
||||
/// if a user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
struct HitCounts(HashMap<String, usize>);
|
||||
|
||||
impl Global for HitCounts {}
|
||||
@@ -158,6 +164,66 @@ impl CommandPaletteDelegate {
|
||||
selected_ix: 0,
|
||||
telemetry,
|
||||
previous_focus_handle,
|
||||
updating_matches: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_updated(
|
||||
&mut self,
|
||||
query: String,
|
||||
mut commands: Vec<Command>,
|
||||
mut matches: Vec<StringMatch>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) {
|
||||
self.updating_matches.take();
|
||||
|
||||
let mut intercept_result =
|
||||
if let Some(interceptor) = cx.try_global::<CommandPaletteInterceptor>() {
|
||||
(interceptor.0)(&query, cx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if parse_zed_link(&query).is_some() {
|
||||
intercept_result = Some(CommandInterceptResult {
|
||||
action: OpenZedUrl { url: query.clone() }.boxed_clone(),
|
||||
string: query.clone(),
|
||||
positions: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
positions,
|
||||
}) = intercept_result
|
||||
{
|
||||
if let Some(idx) = matches
|
||||
.iter()
|
||||
.position(|m| commands[m.candidate_id].action.type_id() == action.type_id())
|
||||
{
|
||||
matches.remove(idx);
|
||||
}
|
||||
commands.push(Command {
|
||||
name: string.clone(),
|
||||
action,
|
||||
});
|
||||
matches.insert(
|
||||
0,
|
||||
StringMatch {
|
||||
candidate_id: commands.len() - 1,
|
||||
string,
|
||||
positions,
|
||||
score: 0.0,
|
||||
},
|
||||
)
|
||||
}
|
||||
self.commands = commands;
|
||||
self.matches = matches;
|
||||
if self.matches.is_empty() {
|
||||
self.selected_ix = 0;
|
||||
} else {
|
||||
self.selected_ix = cmp::min(self.selected_ix, self.matches.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,113 +252,99 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let mut commands = self.all_commands.clone();
|
||||
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
cx.read_global::<HitCounts, _>(|hit_counts, _| {
|
||||
let (mut tx, mut rx) = postage::dispatch::channel(1);
|
||||
let task = cx.background_executor().spawn({
|
||||
let mut commands = self.all_commands.clone();
|
||||
let hit_counts = cx.global::<HitCounts>().clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
let query = query.clone();
|
||||
async move {
|
||||
commands.sort_by_key(|action| {
|
||||
(
|
||||
Reverse(hit_counts.0.get(&action.name).cloned()),
|
||||
action.name.clone(),
|
||||
)
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
|
||||
let candidates = commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: command.name.to_string(),
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
let candidates = commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: command.name.to_string(),
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let ret = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
ret
|
||||
};
|
||||
|
||||
tx.send((commands, matches)).await.log_err();
|
||||
}
|
||||
});
|
||||
self.updating_matches = Some((task, rx.clone()));
|
||||
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
let Some((commands, matches)) = rx.recv().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut intercept_result = cx
|
||||
.try_read_global(|interceptor: &CommandPaletteInterceptor, cx| {
|
||||
(interceptor.0)(&query, cx)
|
||||
})
|
||||
.flatten();
|
||||
let release_channel = cx
|
||||
.update(|cx| ReleaseChannel::try_global(cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
if release_channel == Some(ReleaseChannel::Dev) {
|
||||
if parse_zed_link(&query).is_some() {
|
||||
intercept_result = Some(CommandInterceptResult {
|
||||
action: OpenZedUrl { url: query.clone() }.boxed_clone(),
|
||||
string: query.clone(),
|
||||
positions: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
positions,
|
||||
}) = intercept_result
|
||||
{
|
||||
if let Some(idx) = matches
|
||||
.iter()
|
||||
.position(|m| commands[m.candidate_id].action.type_id() == action.type_id())
|
||||
{
|
||||
matches.remove(idx);
|
||||
}
|
||||
commands.push(Command {
|
||||
name: string.clone(),
|
||||
action,
|
||||
});
|
||||
matches.insert(
|
||||
0,
|
||||
StringMatch {
|
||||
candidate_id: commands.len() - 1,
|
||||
string,
|
||||
positions,
|
||||
score: 0.0,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.commands = commands;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_ix = 0;
|
||||
} else {
|
||||
delegate.selected_ix =
|
||||
cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
|
||||
}
|
||||
.update(&mut cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.matches_updated(query, commands, matches, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn finalize_update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
duration: Duration,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> bool {
|
||||
let Some((task, rx)) = self.updating_matches.take() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
match cx
|
||||
.background_executor()
|
||||
.block_with_timeout(duration, rx.clone().recv())
|
||||
{
|
||||
Ok(Some((commands, matches))) => {
|
||||
self.matches_updated(query, commands, matches, cx);
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
self.updating_matches = Some((task, rx));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
|
||||
@@ -38,6 +38,9 @@ smol.workspace = true
|
||||
theme.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
[dev-dependencies]
|
||||
clock.workspace = true
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -428,6 +428,8 @@ impl Copilot {
|
||||
let binary = LanguageServerBinary {
|
||||
path: node_path,
|
||||
arguments,
|
||||
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
|
||||
env: None,
|
||||
};
|
||||
|
||||
let server = LanguageServer::new(
|
||||
@@ -512,7 +514,7 @@ impl Copilot {
|
||||
.await?;
|
||||
match sign_in {
|
||||
request::SignInInitiateResult::AlreadySignedIn { user } => {
|
||||
Ok(request::SignInStatus::Ok { user })
|
||||
Ok(request::SignInStatus::Ok { user: Some(user) })
|
||||
}
|
||||
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -920,7 +922,7 @@ impl Copilot {
|
||||
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
match lsp_status {
|
||||
request::SignInStatus::Ok { .. }
|
||||
request::SignInStatus::Ok { user: Some(_) }
|
||||
| request::SignInStatus::MaybeOk { .. }
|
||||
| request::SignInStatus::AlreadySignedIn { .. } => {
|
||||
server.sign_in_status = SignInStatus::Authorized;
|
||||
@@ -936,7 +938,7 @@ impl Copilot {
|
||||
self.unregister_buffer(&buffer);
|
||||
}
|
||||
}
|
||||
request::SignInStatus::NotSignedIn => {
|
||||
request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => {
|
||||
server.sign_in_status = SignInStatus::SignedOut;
|
||||
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(&buffer);
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct SignInConfirmParams {
|
||||
pub enum SignInStatus {
|
||||
#[serde(rename = "OK")]
|
||||
Ok {
|
||||
user: String,
|
||||
user: Option<String>,
|
||||
},
|
||||
MaybeOk {
|
||||
user: String,
|
||||
|
||||
@@ -885,7 +885,7 @@ mod tests {
|
||||
use super::*;
|
||||
use editor::{
|
||||
display_map::{BlockContext, TransformBlock},
|
||||
DisplayPoint,
|
||||
DisplayPoint, GutterDimensions,
|
||||
};
|
||||
use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
|
||||
@@ -1599,8 +1599,7 @@ mod tests {
|
||||
.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x: px(0.),
|
||||
gutter_padding: px(0.),
|
||||
gutter_width: px(0.),
|
||||
gutter_dimensions: &GutterDimensions::default(),
|
||||
line_height: px(0.),
|
||||
em_width: px(0.),
|
||||
max_width: px(0.),
|
||||
|
||||
@@ -40,7 +40,7 @@ indoc = "1.0.4"
|
||||
itertools = "0.10"
|
||||
language.workspace = true
|
||||
lazy_static.workspace = true
|
||||
linkify = "0.10.0"
|
||||
linkify.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
|
||||
@@ -165,6 +165,8 @@ gpui::actions!(
|
||||
GoToPrevHunk,
|
||||
GoToTypeDefinition,
|
||||
GoToTypeDefinitionSplit,
|
||||
GoToImplementation,
|
||||
GoToImplementationSplit,
|
||||
OpenUrl,
|
||||
HalfPageDown,
|
||||
HalfPageUp,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{
|
||||
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
|
||||
Highlights,
|
||||
};
|
||||
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, ToPoint as _};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{AnyElement, ElementContext, Pixels, View};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
@@ -88,8 +88,7 @@ pub struct BlockContext<'a, 'b> {
|
||||
pub view: View<Editor>,
|
||||
pub anchor_x: Pixels,
|
||||
pub max_width: Pixels,
|
||||
pub gutter_width: Pixels,
|
||||
pub gutter_padding: Pixels,
|
||||
pub gutter_dimensions: &'b GutterDimensions,
|
||||
pub em_width: Pixels,
|
||||
pub line_height: Pixels,
|
||||
pub block_id: usize,
|
||||
|
||||
@@ -88,6 +88,7 @@ pub use multi_buffer::{
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::*;
|
||||
@@ -443,7 +444,8 @@ pub struct EditorSnapshot {
|
||||
}
|
||||
|
||||
pub struct GutterDimensions {
|
||||
pub padding: Pixels,
|
||||
pub left_padding: Pixels,
|
||||
pub right_padding: Pixels,
|
||||
pub width: Pixels,
|
||||
pub margin: Pixels,
|
||||
}
|
||||
@@ -451,7 +453,8 @@ pub struct GutterDimensions {
|
||||
impl Default for GutterDimensions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
padding: Pixels::ZERO,
|
||||
left_padding: Pixels::ZERO,
|
||||
right_padding: Pixels::ZERO,
|
||||
width: Pixels::ZERO,
|
||||
margin: Pixels::ZERO,
|
||||
}
|
||||
@@ -1346,6 +1349,7 @@ pub(crate) struct NavigationData {
|
||||
enum GotoDefinitionKind {
|
||||
Symbol,
|
||||
Type,
|
||||
Implementation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -4057,7 +4061,8 @@ impl Editor {
|
||||
if self.available_code_actions.is_some() {
|
||||
Some(
|
||||
IconButton::new("code_actions_indicator", ui::IconName::Bolt)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(Color::Muted)
|
||||
.selected(is_active)
|
||||
.on_click(cx.listener(|editor, _e, cx| {
|
||||
@@ -4206,8 +4211,43 @@ impl Editor {
|
||||
active_index: 0,
|
||||
ranges: tabstops,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the just-entered snippet ends with an auto-closable bracket.
|
||||
if self.autoclose_regions.is_empty() {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
for selection in &mut self.selections.all::<Point>(cx) {
|
||||
let selection_head = selection.head();
|
||||
let Some(scope) = snapshot.language_scope_at(selection_head) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut bracket_pair = None;
|
||||
let next_chars = snapshot.chars_at(selection_head).collect::<String>();
|
||||
let prev_chars = snapshot
|
||||
.reversed_chars_at(selection_head)
|
||||
.collect::<String>();
|
||||
for (pair, enabled) in scope.brackets() {
|
||||
if enabled
|
||||
&& pair.close
|
||||
&& prev_chars.starts_with(pair.start.as_str())
|
||||
&& next_chars.starts_with(pair.end.as_str())
|
||||
{
|
||||
bracket_pair = Some(pair.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(pair) = bracket_pair {
|
||||
let start = snapshot.anchor_after(selection_head);
|
||||
let end = snapshot.anchor_after(selection_head);
|
||||
self.autoclose_regions.push(AutocloseRegion {
|
||||
selection_id: selection.id,
|
||||
range: start..end,
|
||||
pair,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7317,6 +7357,18 @@ impl Editor {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_implementation(&mut self, _: &GoToImplementation, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_implementation_split(
|
||||
&mut self,
|
||||
_: &GoToImplementationSplit,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx);
|
||||
}
|
||||
@@ -7354,12 +7406,14 @@ impl Editor {
|
||||
let definitions = project.update(cx, |project, cx| match kind {
|
||||
GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
|
||||
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
|
||||
GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx),
|
||||
});
|
||||
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let definitions = definitions.await?;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.navigate_to_hover_links(
|
||||
Some(kind),
|
||||
definitions.into_iter().map(HoverLink::Text).collect(),
|
||||
split,
|
||||
cx,
|
||||
@@ -7392,8 +7446,9 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn navigate_to_hover_links(
|
||||
pub(crate) fn navigate_to_hover_links(
|
||||
&mut self,
|
||||
kind: Option<GotoDefinitionKind>,
|
||||
mut definitions: Vec<HoverLink>,
|
||||
split: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
@@ -7462,13 +7517,18 @@ impl Editor {
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let (title, location_tasks, workspace) = editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let tab_kind = match kind {
|
||||
Some(GotoDefinitionKind::Implementation) => "Implementations",
|
||||
_ => "Definitions",
|
||||
};
|
||||
let title = definitions
|
||||
.iter()
|
||||
.find_map(|definition| match definition {
|
||||
HoverLink::Text(link) => link.origin.as_ref().map(|origin| {
|
||||
let buffer = origin.buffer.read(cx);
|
||||
format!(
|
||||
"Definitions for {}",
|
||||
"{} for {}",
|
||||
tab_kind,
|
||||
buffer
|
||||
.text_for_range(origin.range.clone())
|
||||
.collect::<String>()
|
||||
@@ -7477,7 +7537,7 @@ impl Editor {
|
||||
HoverLink::InlayHint(_, _) => None,
|
||||
HoverLink::Url(_) => None,
|
||||
})
|
||||
.unwrap_or("Definitions".to_string());
|
||||
.unwrap_or(tab_kind.to_string());
|
||||
let location_tasks = definitions
|
||||
.into_iter()
|
||||
.map(|definition| match definition {
|
||||
@@ -9580,23 +9640,50 @@ impl EditorSnapshot {
|
||||
max_line_number_width: Pixels,
|
||||
cx: &AppContext,
|
||||
) -> GutterDimensions {
|
||||
if self.show_gutter {
|
||||
let descent = cx.text_system().descent(font_id, font_size);
|
||||
let gutter_padding_factor = 4.0;
|
||||
let gutter_padding = (em_width * gutter_padding_factor).round();
|
||||
if !self.show_gutter {
|
||||
return GutterDimensions::default();
|
||||
}
|
||||
let descent = cx.text_system().descent(font_id, font_size);
|
||||
|
||||
let show_git_gutter = matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
);
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
let line_gutter_width = if gutter_settings.line_numbers {
|
||||
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
|
||||
let min_width_for_number_on_gutter = em_width * 4.0;
|
||||
let gutter_width =
|
||||
max_line_number_width.max(min_width_for_number_on_gutter) + gutter_padding * 2.0;
|
||||
let gutter_margin = -descent;
|
||||
|
||||
GutterDimensions {
|
||||
padding: gutter_padding,
|
||||
width: gutter_width,
|
||||
margin: gutter_margin,
|
||||
}
|
||||
max_line_number_width.max(min_width_for_number_on_gutter)
|
||||
} else {
|
||||
GutterDimensions::default()
|
||||
0.0.into()
|
||||
};
|
||||
|
||||
let left_padding = if gutter_settings.code_actions {
|
||||
em_width * 3.0
|
||||
} else if show_git_gutter && gutter_settings.line_numbers {
|
||||
em_width * 2.0
|
||||
} else if show_git_gutter || gutter_settings.line_numbers {
|
||||
em_width
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
let right_padding = if gutter_settings.folds && gutter_settings.line_numbers {
|
||||
em_width * 4.0
|
||||
} else if gutter_settings.folds {
|
||||
em_width * 3.0
|
||||
} else if gutter_settings.line_numbers {
|
||||
em_width
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
GutterDimensions {
|
||||
left_padding,
|
||||
right_padding,
|
||||
width: line_gutter_width + left_padding + right_padding,
|
||||
margin: -descent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10103,9 +10190,14 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
.group(group_id.clone())
|
||||
.relative()
|
||||
.size_full()
|
||||
.pl(cx.gutter_width)
|
||||
.w(cx.max_width + cx.gutter_width)
|
||||
.child(div().flex().w(cx.anchor_x - cx.gutter_width).flex_shrink())
|
||||
.pl(cx.gutter_dimensions.width)
|
||||
.w(cx.max_width + cx.gutter_dimensions.width)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.w(cx.anchor_x - cx.gutter_dimensions.width)
|
||||
.flex_shrink(),
|
||||
)
|
||||
.child(div().flex().flex_shrink_0().child(
|
||||
StyledText::new(text_without_backticks.clone()).with_highlights(
|
||||
&text_style,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct EditorSettings {
|
||||
pub use_on_type_format: bool,
|
||||
pub toolbar: Toolbar,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub gutter: Gutter,
|
||||
pub vertical_scroll_margin: f32,
|
||||
pub relative_line_numbers: bool,
|
||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||
@@ -45,6 +46,13 @@ pub struct Scrollbar {
|
||||
pub diagnostics: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Gutter {
|
||||
pub line_numbers: bool,
|
||||
pub code_actions: bool,
|
||||
pub folds: bool,
|
||||
}
|
||||
|
||||
/// When to show the scrollbar in the editor.
|
||||
///
|
||||
/// Default: auto
|
||||
@@ -97,6 +105,8 @@ pub struct EditorSettingsContent {
|
||||
pub toolbar: Option<ToolbarContent>,
|
||||
/// Scrollbar related settings
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
/// Gutter related settings
|
||||
pub gutter: Option<GutterContent>,
|
||||
|
||||
/// The number of lines to keep above/below the cursor when auto-scrolling.
|
||||
///
|
||||
@@ -157,6 +167,23 @@ pub struct ScrollbarContent {
|
||||
pub diagnostics: Option<bool>,
|
||||
}
|
||||
|
||||
/// Gutter related settings
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct GutterContent {
|
||||
/// Whether to show line numbers in the gutter.
|
||||
///
|
||||
/// Default: true
|
||||
pub line_numbers: Option<bool>,
|
||||
/// Whether to show code action buttons in the gutter.
|
||||
///
|
||||
/// Default: true
|
||||
pub code_actions: Option<bool>,
|
||||
/// Whether to show fold buttons in the gutter.
|
||||
///
|
||||
/// Default: true
|
||||
pub folds: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for EditorSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ use crate::{
|
||||
mouse_context_menu,
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, HalfPageDown, HalfPageUp, HoveredCursor, LineDown,
|
||||
LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, GutterDimensions, HalfPageDown, HalfPageUp,
|
||||
HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection,
|
||||
SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
@@ -260,6 +260,8 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::go_to_prev_hunk);
|
||||
register_action(view, cx, Editor::go_to_definition);
|
||||
register_action(view, cx, Editor::go_to_definition_split);
|
||||
register_action(view, cx, Editor::go_to_implementation);
|
||||
register_action(view, cx, Editor::go_to_implementation_split);
|
||||
register_action(view, cx, Editor::go_to_type_definition);
|
||||
register_action(view, cx, Editor::go_to_type_definition_split);
|
||||
register_action(view, cx, Editor::open_url);
|
||||
@@ -630,8 +632,8 @@ impl EditorElement {
|
||||
let scroll_top =
|
||||
layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height;
|
||||
let gutter_bg = cx.theme().colors().editor_gutter_background;
|
||||
cx.paint_quad(fill(gutter_bounds, gutter_bg));
|
||||
cx.paint_quad(fill(text_bounds, self.style.background));
|
||||
cx.paint_quad(fill(gutter_bounds, gutter_bg), None, None);
|
||||
cx.paint_quad(fill(text_bounds, self.style.background), None, None);
|
||||
|
||||
if let EditorMode::Full = layout.mode {
|
||||
let mut active_rows = layout.active_rows.iter().peekable();
|
||||
@@ -655,7 +657,7 @@ impl EditorElement {
|
||||
layout.position_map.line_height * (end_row - start_row + 1) as f32,
|
||||
);
|
||||
let active_line_bg = cx.theme().colors().editor_active_line_background;
|
||||
cx.paint_quad(fill(Bounds { origin, size }, active_line_bg));
|
||||
cx.paint_quad(fill(Bounds { origin, size }, active_line_bg), None, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,7 +673,11 @@ impl EditorElement {
|
||||
layout.position_map.line_height * highlighted_rows.len() as f32,
|
||||
);
|
||||
let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background;
|
||||
cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg));
|
||||
cx.paint_quad(
|
||||
fill(Bounds { origin, size }, highlighted_line_bg),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let scroll_left =
|
||||
@@ -692,13 +698,17 @@ impl EditorElement {
|
||||
} else {
|
||||
cx.theme().colors().editor_wrap_guide
|
||||
};
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: point(x, text_bounds.origin.y),
|
||||
size: size(px(1.), text_bounds.size.height),
|
||||
},
|
||||
color,
|
||||
));
|
||||
cx.paint_quad(
|
||||
fill(
|
||||
Bounds {
|
||||
origin: point(x, text_bounds.origin.y),
|
||||
size: size(px(1.), text_bounds.size.height),
|
||||
},
|
||||
color,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -714,20 +724,22 @@ impl EditorElement {
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let scroll_top = scroll_position.y * line_height;
|
||||
|
||||
let show_gutter = matches!(
|
||||
let show_git_gutter = matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
);
|
||||
|
||||
if show_gutter {
|
||||
if show_git_gutter {
|
||||
Self::paint_diff_hunks(bounds, layout, cx);
|
||||
}
|
||||
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
for (ix, line) in layout.line_numbers.iter().enumerate() {
|
||||
if let Some(line) = line {
|
||||
let line_origin = bounds.origin
|
||||
+ point(
|
||||
bounds.size.width - line.width - layout.gutter_padding,
|
||||
bounds.size.width - line.width - layout.gutter_dimensions.right_padding,
|
||||
ix as f32 * line_height - (scroll_top % line_height),
|
||||
);
|
||||
|
||||
@@ -738,6 +750,7 @@ impl EditorElement {
|
||||
cx.with_z_index(1, |cx| {
|
||||
for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
|
||||
if let Some(fold_indicator) = fold_indicator {
|
||||
debug_assert!(gutter_settings.folds);
|
||||
let mut fold_indicator = fold_indicator.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
@@ -746,11 +759,12 @@ impl EditorElement {
|
||||
let fold_indicator_size = fold_indicator.measure(available_space, cx);
|
||||
|
||||
let position = point(
|
||||
bounds.size.width - layout.gutter_padding,
|
||||
bounds.size.width - layout.gutter_dimensions.right_padding,
|
||||
ix as f32 * line_height - (scroll_top % line_height),
|
||||
);
|
||||
let centering_offset = point(
|
||||
(layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width)
|
||||
(layout.gutter_dimensions.right_padding + layout.gutter_dimensions.margin
|
||||
- fold_indicator_size.width)
|
||||
/ 2.,
|
||||
(line_height - fold_indicator_size.height) / 2.,
|
||||
);
|
||||
@@ -760,6 +774,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if let Some(indicator) = layout.code_actions_indicator.take() {
|
||||
debug_assert!(gutter_settings.code_actions);
|
||||
let mut button = indicator.button.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
@@ -770,7 +785,9 @@ impl EditorElement {
|
||||
let mut x = Pixels::ZERO;
|
||||
let mut y = indicator.row as f32 * line_height - scroll_top;
|
||||
// Center indicator.
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
|
||||
x += (layout.gutter_dimensions.margin + layout.gutter_dimensions.left_padding
|
||||
- indicator_size.width)
|
||||
/ 2.;
|
||||
y += (line_height - indicator_size.height) / 2.;
|
||||
|
||||
button.draw(bounds.origin + point(x, y), available_space, cx);
|
||||
@@ -795,13 +812,17 @@ impl EditorElement {
|
||||
let highlight_origin = bounds.origin + point(-width, start_y);
|
||||
let highlight_size = size(width * 2., end_y - start_y);
|
||||
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
|
||||
cx.paint_quad(quad(
|
||||
highlight_bounds,
|
||||
Corners::all(1. * line_height),
|
||||
cx.theme().status().modified,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
highlight_bounds,
|
||||
Corners::all(1. * line_height),
|
||||
cx.theme().status().modified,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -828,13 +849,17 @@ impl EditorElement {
|
||||
let highlight_origin = bounds.origin + point(-width, start_y);
|
||||
let highlight_size = size(width * 2., end_y - start_y);
|
||||
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
|
||||
cx.paint_quad(quad(
|
||||
highlight_bounds,
|
||||
Corners::all(1. * line_height),
|
||||
cx.theme().status().deleted,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
highlight_bounds,
|
||||
Corners::all(1. * line_height),
|
||||
cx.theme().status().deleted,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -868,13 +893,17 @@ impl EditorElement {
|
||||
let highlight_origin = bounds.origin + point(-width, start_y);
|
||||
let highlight_size = size(width * 2., end_y - start_y);
|
||||
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
|
||||
cx.paint_quad(quad(
|
||||
highlight_bounds,
|
||||
Corners::all(0.05 * line_height),
|
||||
color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
highlight_bounds,
|
||||
Corners::all(0.05 * line_height),
|
||||
color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,7 +914,8 @@ impl EditorElement {
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||
let content_origin =
|
||||
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
|
||||
let line_end_overshoot = 0.15 * layout.position_map.line_height;
|
||||
let whitespace_setting = self
|
||||
.editor
|
||||
@@ -1154,7 +1184,8 @@ impl EditorElement {
|
||||
layout: &LayoutState,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||
let content_origin =
|
||||
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
|
||||
let line_end_overshoot = layout.line_end_overshoot();
|
||||
|
||||
// A softer than perfect black
|
||||
@@ -1180,7 +1211,8 @@ impl EditorElement {
|
||||
layout: &mut LayoutState,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||
let content_origin =
|
||||
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
if let Some((position, mut context_menu)) = layout.context_menu.take() {
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
@@ -1334,18 +1366,22 @@ impl EditorElement {
|
||||
let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
|
||||
|
||||
if layout.show_scrollbars {
|
||||
cx.paint_quad(quad(
|
||||
track_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_track_background,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: Pixels::ZERO,
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_track_border,
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
track_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_track_background,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: Pixels::ZERO,
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_track_border,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
if layout.is_singleton && scrollbar_settings.selections {
|
||||
let start_anchor = Anchor::min();
|
||||
@@ -1365,18 +1401,22 @@ impl EditorElement {
|
||||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1403,18 +1443,22 @@ impl EditorElement {
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1446,18 +1490,22 @@ impl EditorElement {
|
||||
DiffHunkStatus::Modified => cx.theme().status().modified,
|
||||
DiffHunkStatus::Removed => cx.theme().status().deleted,
|
||||
};
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
color,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
color,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1504,33 +1552,41 @@ impl EditorElement {
|
||||
DiagnosticSeverity::INFORMATION => cx.theme().status().info,
|
||||
_ => cx.theme().status().hint,
|
||||
};
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
color,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
color,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cx.paint_quad(quad(
|
||||
thumb_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_thumb_background,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
cx.paint_quad(
|
||||
quad(
|
||||
thumb_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_thumb_background,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let interactive_track_bounds = InteractiveBounds {
|
||||
@@ -1817,7 +1873,10 @@ impl EditorElement {
|
||||
Vec<Option<(FoldStatus, BufferRow, bool)>>,
|
||||
) {
|
||||
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
|
||||
let include_line_numbers = snapshot.mode == EditorMode::Full;
|
||||
let include_line_numbers =
|
||||
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full;
|
||||
let include_fold_statuses =
|
||||
EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full;
|
||||
let mut shaped_line_numbers = Vec::with_capacity(rows.len());
|
||||
let mut fold_statuses = Vec::with_capacity(rows.len());
|
||||
let mut line_number = String::new();
|
||||
@@ -1862,6 +1921,8 @@ impl EditorElement {
|
||||
.shape_line(line_number.clone().into(), font_size, &[run])
|
||||
.unwrap();
|
||||
shaped_line_numbers.push(Some(shaped_line));
|
||||
}
|
||||
if include_fold_statuses {
|
||||
fold_statuses.push(
|
||||
is_singleton
|
||||
.then(|| {
|
||||
@@ -1958,7 +2019,13 @@ impl EditorElement {
|
||||
.unwrap()
|
||||
.width;
|
||||
|
||||
let gutter_dimensions = snapshot.gutter_dimensions(font_id, font_size, em_width, self.max_line_number_width(&snapshot, cx), cx);
|
||||
let gutter_dimensions = snapshot.gutter_dimensions(
|
||||
font_id,
|
||||
font_size,
|
||||
em_width,
|
||||
self.max_line_number_width(&snapshot, cx),
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.gutter_width = gutter_dimensions.width;
|
||||
|
||||
@@ -2211,8 +2278,7 @@ impl EditorElement {
|
||||
bounds.size.width,
|
||||
scroll_width,
|
||||
text_width,
|
||||
gutter_dimensions.padding,
|
||||
gutter_dimensions.width,
|
||||
&gutter_dimensions,
|
||||
em_width,
|
||||
gutter_dimensions.width + gutter_dimensions.margin,
|
||||
line_height,
|
||||
@@ -2249,6 +2315,8 @@ impl EditorElement {
|
||||
snapshot = editor.snapshot(cx);
|
||||
}
|
||||
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
let mut context_menu = None;
|
||||
let mut code_actions_indicator = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
@@ -2270,12 +2338,14 @@ impl EditorElement {
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
|
||||
code_actions_indicator = editor
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|element| CodeActionsIndicator {
|
||||
row: newest_selection_head.row(),
|
||||
button: element,
|
||||
});
|
||||
if gutter_settings.code_actions {
|
||||
code_actions_indicator = editor
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|element| CodeActionsIndicator {
|
||||
row: newest_selection_head.row(),
|
||||
button: element,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2293,29 +2363,32 @@ impl EditorElement {
|
||||
None
|
||||
} else {
|
||||
editor.hover_state.render(
|
||||
&snapshot,
|
||||
&style,
|
||||
visible_rows,
|
||||
max_size,
|
||||
editor.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
)
|
||||
&snapshot,
|
||||
&style,
|
||||
visible_rows,
|
||||
max_size,
|
||||
editor.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
)
|
||||
};
|
||||
|
||||
let editor_view = cx.view().clone();
|
||||
let fold_indicators = cx.with_element_context(|cx| {
|
||||
|
||||
cx.with_element_id(Some("gutter_fold_indicators"), |_cx| {
|
||||
editor.render_fold_indicators(
|
||||
fold_statuses,
|
||||
&style,
|
||||
editor.gutter_hovered,
|
||||
line_height,
|
||||
gutter_dimensions.margin,
|
||||
editor_view,
|
||||
)
|
||||
})
|
||||
});
|
||||
let fold_indicators = if gutter_settings.folds {
|
||||
cx.with_element_context(|cx| {
|
||||
cx.with_element_id(Some("gutter_fold_indicators"), |_cx| {
|
||||
editor.render_fold_indicators(
|
||||
fold_statuses,
|
||||
&style,
|
||||
editor.gutter_hovered,
|
||||
line_height,
|
||||
gutter_dimensions.margin,
|
||||
editor_view,
|
||||
)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let invisible_symbol_font_size = font_size / 2.;
|
||||
let tab_invisible = cx
|
||||
@@ -2368,13 +2441,12 @@ impl EditorElement {
|
||||
visible_display_row_range: start_row..end_row,
|
||||
wrap_guides,
|
||||
gutter_size,
|
||||
gutter_padding: gutter_dimensions.padding,
|
||||
gutter_dimensions,
|
||||
text_size,
|
||||
scrollbar_row_range,
|
||||
show_scrollbars,
|
||||
is_singleton,
|
||||
max_row,
|
||||
gutter_margin: gutter_dimensions.margin,
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
@@ -2401,8 +2473,7 @@ impl EditorElement {
|
||||
editor_width: Pixels,
|
||||
scroll_width: Pixels,
|
||||
text_width: Pixels,
|
||||
gutter_padding: Pixels,
|
||||
gutter_width: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
em_width: Pixels,
|
||||
text_x: Pixels,
|
||||
line_height: Pixels,
|
||||
@@ -2445,9 +2516,8 @@ impl EditorElement {
|
||||
block.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x,
|
||||
gutter_padding,
|
||||
gutter_dimensions,
|
||||
line_height,
|
||||
gutter_width,
|
||||
em_width,
|
||||
block_id,
|
||||
max_width: scroll_width.max(text_width),
|
||||
@@ -2551,12 +2621,14 @@ impl EditorElement {
|
||||
h_flex()
|
||||
.id(("collapsed context", block_id))
|
||||
.size_full()
|
||||
.gap(gutter_padding)
|
||||
.gap(gutter_dimensions.left_padding + gutter_dimensions.right_padding)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.w(gutter_width - gutter_padding)
|
||||
.w(gutter_dimensions.width
|
||||
- (gutter_dimensions.left_padding
|
||||
+ gutter_dimensions.right_padding))
|
||||
.h_full()
|
||||
.text_buffer(cx)
|
||||
.text_color(cx.theme().colors().editor_line_number)
|
||||
@@ -2617,7 +2689,7 @@ impl EditorElement {
|
||||
BlockStyle::Sticky => editor_width,
|
||||
BlockStyle::Flex => editor_width
|
||||
.max(fixed_block_max_width)
|
||||
.max(gutter_width + scroll_width),
|
||||
.max(gutter_dimensions.width + scroll_width),
|
||||
BlockStyle::Fixed => unreachable!(),
|
||||
};
|
||||
let available_space = size(
|
||||
@@ -2634,7 +2706,7 @@ impl EditorElement {
|
||||
});
|
||||
}
|
||||
(
|
||||
scroll_width.max(fixed_block_max_width - gutter_width),
|
||||
scroll_width.max(fixed_block_max_width - gutter_dimensions.width),
|
||||
blocks,
|
||||
)
|
||||
}
|
||||
@@ -3151,8 +3223,7 @@ type BufferRow = u32;
|
||||
pub struct LayoutState {
|
||||
position_map: Arc<PositionMap>,
|
||||
gutter_size: Size<Pixels>,
|
||||
gutter_padding: Pixels,
|
||||
gutter_margin: Pixels,
|
||||
gutter_dimensions: GutterDimensions,
|
||||
text_size: gpui::Size<Pixels>,
|
||||
mode: EditorMode,
|
||||
wrap_guides: SmallVec<[(Pixels, bool); 2]>,
|
||||
@@ -3394,7 +3465,7 @@ impl Cursor {
|
||||
})
|
||||
}
|
||||
|
||||
cx.paint_quad(cursor);
|
||||
cx.paint_quad(cursor, None, None);
|
||||
|
||||
if let Some(block_text) = &self.block_text {
|
||||
block_text
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Editor {
|
||||
cx.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
self.navigate_to_hover_links(hovered_link_state.links, modifiers.alt, cx);
|
||||
self.navigate_to_hover_links(None, hovered_link_state.links, modifiers.alt, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
|
||||
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
|
||||
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
};
|
||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||
|
||||
@@ -48,6 +48,7 @@ pub fn deploy_context_menu(
|
||||
menu.action("Rename Symbol", Box::new(Rename))
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||
.action("Go to Implementation", Box::new(GoToImplementation))
|
||||
.action("Find All References", Box::new(FindAllReferences))
|
||||
.action(
|
||||
"Code Actions",
|
||||
|
||||
@@ -147,7 +147,7 @@ impl EditorTestContext {
|
||||
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
|
||||
self.cx.dispatch_keystroke(self.window, keystroke, false);
|
||||
self.cx.dispatch_keystroke(self.window, keystroke);
|
||||
|
||||
keystroke_under_test_handle
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ pub struct ExtensionsApiResponse {
|
||||
pub data: Vec<Extension>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Extension {
|
||||
pub id: Arc<str>,
|
||||
pub version: Arc<str>,
|
||||
@@ -651,6 +651,12 @@ impl ExtensionStore {
|
||||
let Ok(relative_path) = language_path.strip_prefix(&extension_dir) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(Some(fs_metadata)) = fs.metadata(&language_path).await else {
|
||||
continue;
|
||||
};
|
||||
if !fs_metadata.is_dir {
|
||||
continue;
|
||||
}
|
||||
let config = fs.load(&language_path.join("config.toml")).await?;
|
||||
let config = ::toml::from_str::<LanguageConfig>(&config)?;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, CheckboxWithLabel, Tooltip};
|
||||
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent},
|
||||
@@ -34,7 +34,8 @@ pub struct ExtensionsPage {
|
||||
list: UniformListScrollHandle,
|
||||
telemetry: Arc<Telemetry>,
|
||||
is_fetching_extensions: bool,
|
||||
extensions_entries: Vec<Extension>,
|
||||
is_only_showing_installed_extensions: bool,
|
||||
extension_entries: Vec<Extension>,
|
||||
query_editor: View<Editor>,
|
||||
query_contains_error: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
@@ -54,7 +55,8 @@ impl ExtensionsPage {
|
||||
list: UniformListScrollHandle::new(),
|
||||
telemetry: workspace.client().telemetry().clone(),
|
||||
is_fetching_extensions: false,
|
||||
extensions_entries: Vec::new(),
|
||||
is_only_showing_installed_extensions: false,
|
||||
extension_entries: Vec::new(),
|
||||
query_contains_error: false,
|
||||
extension_fetch_task: None,
|
||||
_subscription: subscription,
|
||||
@@ -65,6 +67,24 @@ impl ExtensionsPage {
|
||||
})
|
||||
}
|
||||
|
||||
fn filtered_extension_entries(&self, cx: &mut ViewContext<Self>) -> Vec<Extension> {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
|
||||
self.extension_entries
|
||||
.iter()
|
||||
.filter(|extension| {
|
||||
if self.is_only_showing_installed_extensions {
|
||||
let status = extension_store.extension_status(&extension.id);
|
||||
|
||||
matches!(status, ExtensionStatus::Installed(_))
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn install_extension(
|
||||
&self,
|
||||
extension_id: Arc<str>,
|
||||
@@ -94,7 +114,7 @@ impl ExtensionsPage {
|
||||
let fetch_result = extensions.await;
|
||||
match fetch_result {
|
||||
Ok(extensions) => this.update(&mut cx, |this, cx| {
|
||||
this.extensions_entries = extensions;
|
||||
this.extension_entries = extensions;
|
||||
this.is_fetching_extensions = false;
|
||||
cx.notify();
|
||||
}),
|
||||
@@ -113,7 +133,7 @@ impl ExtensionsPage {
|
||||
}
|
||||
|
||||
fn render_extensions(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>) -> Vec<Div> {
|
||||
self.extensions_entries[range]
|
||||
self.filtered_extension_entries(cx)[range]
|
||||
.iter()
|
||||
.map(|extension| self.render_entry(extension, cx))
|
||||
.collect()
|
||||
@@ -195,6 +215,7 @@ impl ExtensionsPage {
|
||||
.color(Color::Accent);
|
||||
|
||||
let repository_url = extension.repository.clone();
|
||||
let tooltip_text = Tooltip::text(repository_url.clone(), cx);
|
||||
|
||||
div().w_full().child(
|
||||
v_flex()
|
||||
@@ -269,7 +290,8 @@ impl ExtensionsPage {
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.open_url(&repository_url);
|
||||
})),
|
||||
}))
|
||||
.tooltip(move |_| tooltip_text.clone()),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -379,10 +401,32 @@ impl ExtensionsPage {
|
||||
Some(search)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let has_search = self.search_query(cx).is_some();
|
||||
|
||||
let message = if self.is_fetching_extensions {
|
||||
"Loading extensions..."
|
||||
} else if self.is_only_showing_installed_extensions {
|
||||
if has_search {
|
||||
"No installed extensions that match your search."
|
||||
} else {
|
||||
"No installed extensions."
|
||||
}
|
||||
} else {
|
||||
if has_search {
|
||||
"No extensions that match your search."
|
||||
} else {
|
||||
"No extensions."
|
||||
}
|
||||
};
|
||||
|
||||
Label::new(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExtensionsPage {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.p_4()
|
||||
@@ -393,25 +437,39 @@ impl Render for ExtensionsPage {
|
||||
.w_full()
|
||||
.child(Headline::new("Extensions").size(HeadlineSize::XLarge)),
|
||||
)
|
||||
.child(h_flex().w_56().child(self.render_search(cx)))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(h_flex().child(self.render_search(cx)))
|
||||
.child(CheckboxWithLabel::new(
|
||||
"installed",
|
||||
Label::new("Only show installed"),
|
||||
if self.is_only_showing_installed_extensions {
|
||||
Selection::Selected
|
||||
} else {
|
||||
Selection::Unselected
|
||||
},
|
||||
cx.listener(|this, selection, _cx| {
|
||||
this.is_only_showing_installed_extensions = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected => false,
|
||||
Selection::Indeterminate => return,
|
||||
}
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(v_flex().size_full().overflow_y_hidden().map(|this| {
|
||||
if self.extensions_entries.is_empty() {
|
||||
let message = if self.is_fetching_extensions {
|
||||
"Loading extensions..."
|
||||
} else if self.search_query(cx).is_some() {
|
||||
"No extensions that match your search."
|
||||
} else {
|
||||
"No extensions."
|
||||
};
|
||||
|
||||
return this.child(Label::new(message));
|
||||
let entries = self.filtered_extension_entries(cx);
|
||||
if entries.is_empty() {
|
||||
return this.child(self.render_empty_state(cx));
|
||||
}
|
||||
|
||||
this.child(
|
||||
canvas({
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.list.clone();
|
||||
let item_count = self.extensions_entries.len();
|
||||
let item_count = entries.len();
|
||||
move |bounds, cx| {
|
||||
uniform_list::<_, Div, _>(
|
||||
view,
|
||||
|
||||
@@ -32,11 +32,17 @@ log.workspace = true
|
||||
libc = "0.2"
|
||||
time.workspace = true
|
||||
|
||||
gpui = { workspace = true, optional = true}
|
||||
gpui = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
notify = "6.1.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ use fsevent::StreamFlags;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use notify::{Config, EventKind, Watcher};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||
use git2::Repository as LibGitRepository;
|
||||
use parking_lot::Mutex;
|
||||
@@ -21,7 +24,6 @@ use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io,
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{Component, Path, PathBuf},
|
||||
pin::Pin,
|
||||
time::{Duration, SystemTime},
|
||||
@@ -239,8 +241,15 @@ impl Fs for RealFs {
|
||||
} else {
|
||||
symlink_metadata
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let inode = metadata.ino();
|
||||
|
||||
#[cfg(windows)]
|
||||
let inode = file_id(path).await?;
|
||||
|
||||
Ok(Some(Metadata {
|
||||
inode: metadata.ino(),
|
||||
inode,
|
||||
mtime: metadata.modified().unwrap(),
|
||||
is_symlink,
|
||||
is_dir: metadata.file_type().is_dir(),
|
||||
@@ -1327,6 +1336,41 @@ pub fn copy_recursive<'a>(
|
||||
.boxed()
|
||||
}
|
||||
|
||||
// todo!(windows)
|
||||
// can we get file id not open the file twice?
|
||||
// https://github.com/rust-lang/rust/issues/63010
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
|
||||
use smol::fs::windows::OpenOptionsExt;
|
||||
use windows_sys::Win32::{
|
||||
Foundation::HANDLE,
|
||||
Storage::FileSystem::{
|
||||
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS,
|
||||
},
|
||||
};
|
||||
|
||||
let file = smol::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
|
||||
.open(path)
|
||||
.await?;
|
||||
|
||||
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
|
||||
// This function supports Windows XP+
|
||||
smol::unblock(move || {
|
||||
let ret = unsafe { GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info) };
|
||||
if ret == 0 {
|
||||
return Err(anyhow!(format!("{}", std::io::Error::last_os_error())));
|
||||
};
|
||||
|
||||
Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -51,6 +51,7 @@ parking = "2.0.0"
|
||||
parking_lot.workspace = true
|
||||
pathfinder_geometry = "0.5"
|
||||
postage.workspace = true
|
||||
profiling.workspace = true
|
||||
rand.workspace = true
|
||||
raw-window-handle = "0.6"
|
||||
refineable.workspace = true
|
||||
@@ -105,7 +106,7 @@ ashpd = "0.7.0"
|
||||
# todo!(linux) - Technically do not use `randr`, but it doesn't compile otherwise
|
||||
xcb = { version = "1.3", features = ["as-raw-xcb-connection", "present", "randr", "xkb"] }
|
||||
wayland-client= { version = "0.31.2" }
|
||||
wayland-protocols = { version = "0.31.2", features = ["client", "staging"] }
|
||||
wayland-protocols = { version = "0.31.2", features = ["client", "staging", "unstable"] }
|
||||
wayland-backend = { version = "0.3.3", features = ["client_system"] }
|
||||
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
|
||||
as-raw-xcb-connection = "1"
|
||||
@@ -115,3 +116,11 @@ blade-macros.workspace = true
|
||||
blade-rwh.workspace = true
|
||||
bytemuck = "1"
|
||||
cosmic-text = "0.10.0"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/hello_world.rs"
|
||||
|
||||
[[example]]
|
||||
name = "image"
|
||||
path = "examples/image/image.rs"
|
||||
|
||||
@@ -94,7 +94,7 @@ fn generate_shader_bindings() -> PathBuf {
|
||||
let mut builder = cbindgen::Builder::new();
|
||||
|
||||
let src_paths = [
|
||||
crate_dir.join("src/scene.rs"),
|
||||
crate_dir.join("src/scene/primitives.rs"),
|
||||
crate_dir.join("src/geometry.rs"),
|
||||
crate_dir.join("src/color.rs"),
|
||||
crate_dir.join("src/window.rs"),
|
||||
|
||||
BIN
crates/gpui/examples/image/app-icon.png
Normal file
BIN
crates/gpui/examples/image/app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
@@ -64,9 +64,8 @@ fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|_cx| ImageShowcase {
|
||||
local_resource: Arc::new(
|
||||
PathBuf::from_str("crates/zed/resources/app-icon.png").unwrap(),
|
||||
),
|
||||
// Relative path to your root project path
|
||||
local_resource: Arc::new(PathBuf::from_str("examples/image/app-icon.png").unwrap()),
|
||||
remote_resource: "https://picsum.photos/512/512".into(),
|
||||
})
|
||||
});
|
||||
@@ -39,7 +39,7 @@ use std::any::{Any, TypeId};
|
||||
/// }
|
||||
/// register_action!(Paste);
|
||||
/// ```
|
||||
pub trait Action: 'static {
|
||||
pub trait Action: 'static + Send {
|
||||
/// Clone the action into a new box
|
||||
fn boxed_clone(&self) -> Box<dyn Action>;
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ pub struct AppContext {
|
||||
pub(crate) entities: EntityMap,
|
||||
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
@@ -285,6 +286,7 @@ impl AppContext {
|
||||
globals_by_type: FxHashMap::default(),
|
||||
entities,
|
||||
new_view_observers: SubscriberSet::new(),
|
||||
window_handles: FxHashMap::default(),
|
||||
windows: SlotMap::with_key(),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
@@ -324,6 +326,7 @@ impl AppContext {
|
||||
}
|
||||
|
||||
self.windows.clear();
|
||||
self.window_handles.clear();
|
||||
self.flush_effects();
|
||||
|
||||
let futures = futures::future::join_all(futures);
|
||||
@@ -468,8 +471,8 @@ impl AppContext {
|
||||
/// To find all windows of a given type, you could filter on
|
||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||
self.windows
|
||||
.values()
|
||||
.filter_map(|window| Some(window.as_ref()?.handle))
|
||||
.keys()
|
||||
.flat_map(|window_id| self.window_handles.get(&window_id).copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -492,6 +495,7 @@ impl AppContext {
|
||||
let mut window = Window::new(handle.into(), options, cx);
|
||||
let root_view = build_root_view(&mut WindowContext::new(cx, &mut window));
|
||||
window.root_view.replace(root_view.into());
|
||||
cx.window_handles.insert(id, window.handle);
|
||||
cx.windows.get_mut(id).unwrap().replace(window);
|
||||
handle
|
||||
})
|
||||
@@ -1239,12 +1243,13 @@ impl Context for AppContext {
|
||||
.get_mut(handle.id)
|
||||
.ok_or_else(|| anyhow!("window not found"))?
|
||||
.take()
|
||||
.unwrap();
|
||||
.ok_or_else(|| anyhow!("window not found"))?;
|
||||
|
||||
let root_view = window.root_view.clone().unwrap();
|
||||
let result = update(root_view, &mut WindowContext::new(cx, &mut window));
|
||||
|
||||
if window.removed {
|
||||
cx.window_handles.remove(&handle.id);
|
||||
cx.windows.remove(handle.id);
|
||||
} else {
|
||||
cx.windows
|
||||
|
||||
@@ -335,7 +335,7 @@ impl TestAppContext {
|
||||
.map(Keystroke::parse)
|
||||
.map(Result::unwrap)
|
||||
{
|
||||
self.dispatch_keystroke(window, keystroke.into(), false);
|
||||
self.dispatch_keystroke(window, keystroke.into());
|
||||
}
|
||||
|
||||
self.background_executor.run_until_parked()
|
||||
@@ -347,21 +347,16 @@ impl TestAppContext {
|
||||
/// This will also run the background executor until it's parked.
|
||||
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
||||
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
|
||||
self.dispatch_keystroke(window, keystroke.into(), false);
|
||||
self.dispatch_keystroke(window, keystroke.into());
|
||||
}
|
||||
|
||||
self.background_executor.run_until_parked()
|
||||
}
|
||||
|
||||
/// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`)
|
||||
pub fn dispatch_keystroke(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
keystroke: Keystroke,
|
||||
is_held: bool,
|
||||
) {
|
||||
self.test_window(window)
|
||||
.simulate_keystroke(keystroke, is_held)
|
||||
pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
|
||||
self.update_window(window, |_, cx| cx.dispatch_keystroke(keystroke))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Returns the `TestWindow` backing the given handle.
|
||||
|
||||
363
crates/gpui/src/bounds_tree.rs
Normal file
363
crates/gpui/src/bounds_tree.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
use crate::{Bounds, Half, Point};
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::Debug,
|
||||
ops::{Add, Sub},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoundsTree<U, T>
|
||||
where
|
||||
U: Default + Clone + Debug,
|
||||
T: Clone + Debug,
|
||||
{
|
||||
root: Option<usize>,
|
||||
nodes: Vec<Node<U, T>>,
|
||||
stack: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<U, T> BoundsTree<U, T>
|
||||
where
|
||||
U: Clone + Debug + PartialOrd + Add<U, Output = U> + Sub<Output = U> + Half + Default,
|
||||
T: Clone + Debug,
|
||||
{
|
||||
pub fn clear(&mut self) {
|
||||
self.root = None;
|
||||
self.nodes.clear();
|
||||
self.stack.clear();
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, new_bounds: Bounds<U>, payload: T) -> u32 {
|
||||
// If the tree is empty, make the root the new leaf.
|
||||
if self.root.is_none() {
|
||||
let new_node = self.push_leaf(new_bounds, payload, 1);
|
||||
self.root = Some(new_node);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Search for the best place to add the new leaf based on heuristics.
|
||||
let mut max_intersecting_ordering = 0;
|
||||
let mut index = self.root.unwrap();
|
||||
while let Node::Internal {
|
||||
left,
|
||||
right,
|
||||
bounds: node_bounds,
|
||||
..
|
||||
} = &mut self.nodes[index]
|
||||
{
|
||||
let left = *left;
|
||||
let right = *right;
|
||||
*node_bounds = node_bounds.union(&new_bounds);
|
||||
self.stack.push(index);
|
||||
|
||||
// Descend to the best-fit child, based on which one would increase
|
||||
// the surface area the least. This attempts to keep the tree balanced
|
||||
// in terms of surface area. If there is an intersection with the other child,
|
||||
// add its keys to the intersections vector.
|
||||
let left_cost = new_bounds
|
||||
.union(&self.nodes[left].bounds())
|
||||
.half_perimeter();
|
||||
let right_cost = new_bounds
|
||||
.union(&self.nodes[right].bounds())
|
||||
.half_perimeter();
|
||||
if left_cost < right_cost {
|
||||
max_intersecting_ordering =
|
||||
self.find_max_ordering(right, &new_bounds, max_intersecting_ordering);
|
||||
index = left;
|
||||
} else {
|
||||
max_intersecting_ordering =
|
||||
self.find_max_ordering(left, &new_bounds, max_intersecting_ordering);
|
||||
index = right;
|
||||
}
|
||||
}
|
||||
|
||||
// We've found a leaf ('index' now refers to a leaf node).
|
||||
// We'll insert a new parent node above the leaf and attach our new leaf to it.
|
||||
let sibling = index;
|
||||
|
||||
// Check for collision with the located leaf node
|
||||
let Node::Leaf {
|
||||
bounds: sibling_bounds,
|
||||
order: sibling_ordering,
|
||||
..
|
||||
} = &self.nodes[index]
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
if sibling_bounds.intersects(&new_bounds) {
|
||||
max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering);
|
||||
}
|
||||
|
||||
let ordering = max_intersecting_ordering + 1;
|
||||
let new_node = self.push_leaf(new_bounds, payload, ordering);
|
||||
let new_parent = self.push_internal(sibling, new_node);
|
||||
|
||||
// If there was an old parent, we need to update its children indices.
|
||||
if let Some(old_parent) = self.stack.last().copied() {
|
||||
let Node::Internal { left, right, .. } = &mut self.nodes[old_parent] else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
if *left == sibling {
|
||||
*left = new_parent;
|
||||
} else {
|
||||
*right = new_parent;
|
||||
}
|
||||
} else {
|
||||
// If the old parent was the root, the new parent is the new root.
|
||||
self.root = Some(new_parent);
|
||||
}
|
||||
|
||||
for node_index in self.stack.drain(..) {
|
||||
let Node::Internal {
|
||||
max_order: max_ordering,
|
||||
..
|
||||
} = &mut self.nodes[node_index]
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
*max_ordering = cmp::max(*max_ordering, ordering);
|
||||
}
|
||||
|
||||
ordering
|
||||
}
|
||||
|
||||
/// Finds all nodes whose bounds contain the given point and pushes their (bounds, payload) pairs onto the result vector.
|
||||
pub(crate) fn find_containing(
|
||||
&mut self,
|
||||
point: &Point<U>,
|
||||
result: &mut Vec<BoundsSearchResult<U, T>>,
|
||||
) {
|
||||
if let Some(mut index) = self.root {
|
||||
self.stack.clear();
|
||||
self.stack.push(index);
|
||||
|
||||
while let Some(current_index) = self.stack.pop() {
|
||||
match &self.nodes[current_index] {
|
||||
Node::Leaf {
|
||||
bounds,
|
||||
order,
|
||||
data,
|
||||
} => {
|
||||
if bounds.contains(point) {
|
||||
result.push(BoundsSearchResult {
|
||||
bounds: bounds.clone(),
|
||||
order: *order,
|
||||
data: data.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Node::Internal {
|
||||
left,
|
||||
right,
|
||||
bounds,
|
||||
..
|
||||
} => {
|
||||
if bounds.contains(point) {
|
||||
self.stack.push(*left);
|
||||
self.stack.push(*right);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_max_ordering(&self, index: usize, bounds: &Bounds<U>, mut max_ordering: u32) -> u32 {
|
||||
match {
|
||||
let this = &self;
|
||||
&this.nodes[index]
|
||||
} {
|
||||
Node::Leaf {
|
||||
bounds: node_bounds,
|
||||
order: ordering,
|
||||
..
|
||||
} => {
|
||||
if bounds.intersects(node_bounds) {
|
||||
max_ordering = cmp::max(*ordering, max_ordering);
|
||||
}
|
||||
}
|
||||
Node::Internal {
|
||||
left,
|
||||
right,
|
||||
bounds: node_bounds,
|
||||
max_order: node_max_ordering,
|
||||
..
|
||||
} => {
|
||||
if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering {
|
||||
let left_max_ordering = self.nodes[*left].max_ordering();
|
||||
let right_max_ordering = self.nodes[*right].max_ordering();
|
||||
if left_max_ordering > right_max_ordering {
|
||||
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
|
||||
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
|
||||
} else {
|
||||
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
|
||||
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
max_ordering
|
||||
}
|
||||
|
||||
fn push_leaf(&mut self, bounds: Bounds<U>, payload: T, order: u32) -> usize {
|
||||
self.nodes.push(Node::Leaf {
|
||||
bounds,
|
||||
data: payload,
|
||||
order,
|
||||
});
|
||||
self.nodes.len() - 1
|
||||
}
|
||||
|
||||
fn push_internal(&mut self, left: usize, right: usize) -> usize {
|
||||
let left_node = &self.nodes[left];
|
||||
let right_node = &self.nodes[right];
|
||||
let new_bounds = left_node.bounds().union(right_node.bounds());
|
||||
let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering());
|
||||
self.nodes.push(Node::Internal {
|
||||
bounds: new_bounds,
|
||||
left,
|
||||
right,
|
||||
max_order: max_ordering,
|
||||
});
|
||||
self.nodes.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, T> Default for BoundsTree<U, T>
|
||||
where
|
||||
U: Default + Clone + Debug,
|
||||
T: Clone + Debug,
|
||||
{
|
||||
fn default() -> Self {
|
||||
BoundsTree {
|
||||
root: None,
|
||||
nodes: Vec::new(),
|
||||
stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Node<U, T>
|
||||
where
|
||||
U: Clone + Default + Debug,
|
||||
T: Clone + Debug,
|
||||
{
|
||||
Leaf {
|
||||
bounds: Bounds<U>,
|
||||
order: u32,
|
||||
data: T,
|
||||
},
|
||||
Internal {
|
||||
left: usize,
|
||||
right: usize,
|
||||
bounds: Bounds<U>,
|
||||
max_order: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl<U, T> Node<U, T>
|
||||
where
|
||||
U: Clone + Default + Debug,
|
||||
T: Clone + Debug,
|
||||
{
|
||||
fn bounds(&self) -> &Bounds<U> {
|
||||
match self {
|
||||
Node::Leaf { bounds, .. } => bounds,
|
||||
Node::Internal { bounds, .. } => bounds,
|
||||
}
|
||||
}
|
||||
|
||||
fn max_ordering(&self) -> u32 {
|
||||
match self {
|
||||
Node::Leaf {
|
||||
order: ordering, ..
|
||||
} => *ordering,
|
||||
Node::Internal {
|
||||
max_order: max_ordering,
|
||||
..
|
||||
} => *max_ordering,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoundsSearchResult<U: Clone + Default + Debug, T> {
|
||||
pub bounds: Bounds<U>,
|
||||
pub order: u32,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Bounds, Point, Size};
|
||||
|
||||
#[test]
|
||||
fn test_insert_and_find_containing() {
|
||||
let mut tree = BoundsTree::<f32, String>::default();
|
||||
let bounds1 = Bounds {
|
||||
origin: Point { x: 0.0, y: 0.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
let bounds2 = Bounds {
|
||||
origin: Point { x: 5.0, y: 5.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
let bounds3 = Bounds {
|
||||
origin: Point { x: 10.0, y: 10.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
|
||||
// Insert bounds into the tree
|
||||
tree.insert(bounds1.clone(), "Payload 1".to_string());
|
||||
tree.insert(bounds2.clone(), "Payload 2".to_string());
|
||||
tree.insert(bounds3.clone(), "Payload 3".to_string());
|
||||
|
||||
// Points for testing
|
||||
let point_inside_bounds1 = Point { x: 1.0, y: 1.0 };
|
||||
let point_inside_bounds1_and_2 = Point { x: 6.0, y: 6.0 };
|
||||
let point_inside_bounds2_and_3 = Point { x: 12.0, y: 12.0 };
|
||||
let point_outside_all_bounds = Point { x: 21.0, y: 21.0 };
|
||||
|
||||
assert!(!bounds1.contains(&point_inside_bounds2_and_3));
|
||||
assert!(!bounds1.contains(&point_outside_all_bounds));
|
||||
assert!(bounds2.contains(&point_inside_bounds1_and_2));
|
||||
assert!(bounds2.contains(&point_inside_bounds2_and_3));
|
||||
assert!(!bounds2.contains(&point_outside_all_bounds));
|
||||
assert!(!bounds3.contains(&point_inside_bounds1));
|
||||
assert!(bounds3.contains(&point_inside_bounds2_and_3));
|
||||
assert!(!bounds3.contains(&point_outside_all_bounds));
|
||||
|
||||
// Test find_containing for different points
|
||||
let mut result = Vec::new();
|
||||
tree.find_containing(&point_inside_bounds1, &mut result);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].data, "Payload 1");
|
||||
|
||||
result.clear();
|
||||
tree.find_containing(&point_inside_bounds1_and_2, &mut result);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.iter().any(|r| r.data == "Payload 1"));
|
||||
assert!(result.iter().any(|r| r.data == "Payload 2"));
|
||||
|
||||
result.clear();
|
||||
tree.find_containing(&point_inside_bounds2_and_3, &mut result);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.iter().any(|r| r.data == "Payload 2"));
|
||||
assert!(result.iter().any(|r| r.data == "Payload 3"));
|
||||
|
||||
result.clear();
|
||||
tree.find_containing(&point_outside_all_bounds, &mut result);
|
||||
assert_eq!(result.len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -338,6 +338,11 @@ impl Hsla {
|
||||
self.a == 0.0
|
||||
}
|
||||
|
||||
/// Returns true if the HSLA color is fully opaque, false otherwise.
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
self.a == 1.0
|
||||
}
|
||||
|
||||
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
|
||||
///
|
||||
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
|
||||
|
||||
@@ -45,7 +45,7 @@ impl Element for Canvas {
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut ElementContext) {
|
||||
style.paint(bounds, cx, |cx| {
|
||||
style.paint(bounds, None, None, cx, |cx| {
|
||||
(self.paint_callback.take().unwrap())(&bounds, cx)
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -152,7 +152,7 @@ impl Element for Img {
|
||||
let size = size(surface.width().into(), surface.height().into());
|
||||
let new_bounds = preserve_aspect_ratio(bounds, size);
|
||||
// TODO: Add support for corner_radii and grayscale.
|
||||
cx.paint_surface(new_bounds, surface);
|
||||
cx.paint_surface(new_bounds, surface, true);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -828,6 +828,28 @@ where
|
||||
y: self.origin.y.clone() + self.size.height.clone().half(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the half perimeter of a rectangle defined by the bounds.
|
||||
///
|
||||
/// The half perimeter is calculated as the sum of the width and the height of the rectangle.
|
||||
/// This method is generic over the type `T` which must implement the `Sub` trait to allow
|
||||
/// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait
|
||||
/// to sum the width and height for the half perimeter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use zed::{Bounds, Point, Size};
|
||||
/// let bounds = Bounds {
|
||||
/// origin: Point { x: 0, y: 0 },
|
||||
/// size: Size { width: 10, height: 20 },
|
||||
/// };
|
||||
/// let half_perimeter = bounds.half_perimeter();
|
||||
/// assert_eq!(half_perimeter, 30);
|
||||
/// ```
|
||||
pub fn half_perimeter(&self) -> T {
|
||||
self.size.width.clone() + self.size.height.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
|
||||
@@ -2617,6 +2639,12 @@ pub trait Half {
|
||||
fn half(&self) -> Self;
|
||||
}
|
||||
|
||||
impl Half for i32 {
|
||||
fn half(&self) -> Self {
|
||||
self / 2
|
||||
}
|
||||
}
|
||||
|
||||
impl Half for f32 {
|
||||
fn half(&self) -> Self {
|
||||
self / 2.
|
||||
|
||||
@@ -70,6 +70,7 @@ mod app;
|
||||
|
||||
mod arena;
|
||||
mod assets;
|
||||
mod bounds_tree;
|
||||
mod color;
|
||||
mod element;
|
||||
mod elements;
|
||||
@@ -117,6 +118,7 @@ pub use anyhow::Result;
|
||||
pub use app::*;
|
||||
pub(crate) use arena::*;
|
||||
pub use assets::*;
|
||||
pub(crate) use bounds_tree::*;
|
||||
pub use color::*;
|
||||
pub use ctor::ctor;
|
||||
pub use element::*;
|
||||
|
||||
@@ -491,8 +491,8 @@ mod test {
|
||||
.update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
|
||||
.unwrap();
|
||||
|
||||
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap(), false);
|
||||
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false);
|
||||
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
|
||||
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
|
||||
|
||||
window
|
||||
.update(cx, |test_view, _| {
|
||||
|
||||
@@ -61,6 +61,10 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||
Rc::new(LinuxPlatform::new())
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||
todo!("windows")
|
||||
}
|
||||
|
||||
pub(crate) trait Platform: 'static {
|
||||
fn background_executor(&self) -> BackgroundExecutor;
|
||||
@@ -412,7 +416,7 @@ impl PlatformInputHandler {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub(crate) fn flush_pending_input(&mut self, input: &str, cx: &mut WindowContext) {
|
||||
pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
|
||||
self.handler.replace_text_in_range(None, input, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ impl PlatformAtlas for BladeAtlas {
|
||||
if let Some(tile) = lock.tiles_by_key.get(key) {
|
||||
Ok(tile.clone())
|
||||
} else {
|
||||
profiling::scope!("new tile");
|
||||
let (size, bytes) = build()?;
|
||||
let tile = lock.allocate(size, key.texture_kind());
|
||||
lock.upload_texture(tile.texture_id, tile.bounds, &bytes);
|
||||
|
||||
@@ -39,6 +39,7 @@ impl BladeBelt {
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece {
|
||||
for &mut (ref rb, ref mut offset) in self.active.iter_mut() {
|
||||
let aligned = offset.next_multiple_of(self.desc.alignment);
|
||||
|
||||
@@ -444,6 +444,7 @@ impl BladeRenderer {
|
||||
self.gpu.metal_layer().unwrap().as_ptr()
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn rasterize_paths(&mut self, paths: &[Path<ScaledPixels>]) {
|
||||
self.path_tiles.clear();
|
||||
let mut vertices_by_texture_id = HashMap::default();
|
||||
@@ -506,7 +507,10 @@ impl BladeRenderer {
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, scene: &Scene) {
|
||||
let frame = self.gpu.acquire_frame();
|
||||
let frame = {
|
||||
profiling::scope!("acquire frame");
|
||||
self.gpu.acquire_frame()
|
||||
};
|
||||
self.command_encoder.start();
|
||||
self.command_encoder.init_texture(frame.texture());
|
||||
|
||||
@@ -529,6 +533,7 @@ impl BladeRenderer {
|
||||
}],
|
||||
depth_stencil: None,
|
||||
}) {
|
||||
profiling::scope!("render pass");
|
||||
for batch in scene.batches() {
|
||||
match batch {
|
||||
PrimitiveBatch::Quads(quads) => {
|
||||
@@ -718,6 +723,7 @@ impl BladeRenderer {
|
||||
self.command_encoder.present(frame);
|
||||
let sync_point = self.gpu.submit(&mut self.command_encoder);
|
||||
|
||||
profiling::scope!("finish");
|
||||
self.instance_belt.flush(&sync_point);
|
||||
self.atlas.after_frame(&sync_point);
|
||||
self.atlas.clear_textures(AtlasTextureKind::Path);
|
||||
|
||||
@@ -215,6 +215,15 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
let quad = b_quads[input.quad_id];
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
|
||||
quad.corner_radii.top_right == 0.0 &&
|
||||
quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 &&
|
||||
quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
|
||||
quad.border_widths.bottom == 0.0) {
|
||||
return input.background_color;
|
||||
}
|
||||
|
||||
let half_size = quad.bounds.size / 2.0;
|
||||
let center = quad.bounds.origin + half_size;
|
||||
let center_to_point = input.position.xy - center;
|
||||
|
||||
@@ -3,6 +3,7 @@ mod client_dispatcher;
|
||||
mod dispatcher;
|
||||
mod platform;
|
||||
mod text_system;
|
||||
mod util;
|
||||
mod wayland;
|
||||
mod x11;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ impl LinuxDispatcher {
|
||||
) -> Self {
|
||||
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
|
||||
let background_thread = thread::spawn(move || {
|
||||
profiling::register_thread!("background");
|
||||
for runnable in background_receiver {
|
||||
let _ignore_panic = panic::catch_unwind(|| runnable.run());
|
||||
}
|
||||
|
||||
@@ -294,7 +294,13 @@ impl Platform for LinuxPlatform {
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
open::that(path);
|
||||
if path.is_dir() {
|
||||
open::that(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(dir);
|
||||
}
|
||||
|
||||
fn on_become_active(&self, callback: Box<dyn FnMut()>) {
|
||||
|
||||
30
crates/gpui/src/platform/linux/util.rs
Normal file
30
crates/gpui/src/platform/linux/util.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
|
||||
use crate::{Keystroke, Modifiers};
|
||||
|
||||
impl Keystroke {
|
||||
pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
|
||||
let key_utf32 = state.key_get_utf32(keycode);
|
||||
let key_utf8 = state.key_get_utf8(keycode);
|
||||
let key_sym = state.key_get_one_sym(keycode);
|
||||
|
||||
let key = match key_sym {
|
||||
Keysym::Return => "enter".to_owned(),
|
||||
Keysym::Prior => "pageup".to_owned(),
|
||||
Keysym::Next => "pagedown".to_owned(),
|
||||
_ => xkb::keysym_get_name(key_sym).to_lowercase(),
|
||||
};
|
||||
|
||||
// Ignore control characters (and DEL) for the purposes of ime_key,
|
||||
// but if key_utf32 is 0 then assume it isn't one
|
||||
let ime_key = ((key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127))
|
||||
&& !key_utf8.is_empty())
|
||||
.then_some(key_utf8);
|
||||
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
ime_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,21 @@ use wayland_protocols::wp::fractional_scale::v1::client::{
|
||||
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
|
||||
};
|
||||
use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter};
|
||||
use wayland_protocols::xdg::decoration::zv1::client::{
|
||||
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use xkbcommon::xkb;
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
|
||||
use crate::platform::linux::client::Client;
|
||||
use crate::platform::linux::wayland::window::WaylandWindow;
|
||||
use crate::platform::linux::wayland::window::{WaylandDecorationState, WaylandWindow};
|
||||
use crate::platform::{LinuxPlatformInner, PlatformWindow};
|
||||
use crate::ScrollDelta;
|
||||
use crate::{
|
||||
platform::linux::wayland::window::WaylandWindowState, AnyWindowHandle, DisplayId, KeyDownEvent,
|
||||
KeyUpEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Pixels, PlatformDisplay, PlatformInput, Point, ScrollWheelEvent, TouchPhase, WindowOptions,
|
||||
NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta,
|
||||
ScrollWheelEvent, TouchPhase, WindowOptions,
|
||||
};
|
||||
|
||||
const MIN_KEYCODE: u32 = 8; // used to convert evdev scancode to xkb scancode
|
||||
@@ -42,6 +44,7 @@ pub(crate) struct WaylandClientState {
|
||||
wm_base: Option<xdg_wm_base::XdgWmBase>,
|
||||
viewporter: Option<wp_viewporter::WpViewporter>,
|
||||
fractional_scale_manager: Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
|
||||
decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
|
||||
windows: Vec<(xdg_surface::XdgSurface, Rc<WaylandWindowState>)>,
|
||||
platform_inner: Rc<LinuxPlatformInner>,
|
||||
wl_seat: Option<wl_seat::WlSeat>,
|
||||
@@ -70,6 +73,7 @@ impl WaylandClient {
|
||||
wm_base: None,
|
||||
viewporter: None,
|
||||
fractional_scale_manager: None,
|
||||
decoration_manager: None,
|
||||
windows: Vec::new(),
|
||||
platform_inner: Rc::clone(&linux_platform_inner),
|
||||
wl_seat: None,
|
||||
@@ -143,6 +147,31 @@ impl Client for WaylandClient {
|
||||
let toplevel = xdg_surface.get_toplevel(&self.qh, ());
|
||||
let wl_surface = Arc::new(wl_surface);
|
||||
|
||||
// Attempt to set up window decorations based on the requested configuration
|
||||
//
|
||||
// Note that wayland compositors may either not support decorations at all, or may
|
||||
// support them but not allow clients to choose whether they are enabled or not.
|
||||
// We attempt to account for these cases here.
|
||||
|
||||
if let Some(decoration_manager) = state.decoration_manager.as_ref() {
|
||||
// The protocol for managing decorations is present at least, but that doesn't
|
||||
// mean that the compositor will allow us to use it.
|
||||
|
||||
let decoration =
|
||||
decoration_manager.get_toplevel_decoration(&toplevel, &self.qh, xdg_surface.id());
|
||||
|
||||
// todo!(linux) - options.titlebar is lacking information required for wayland.
|
||||
// Especially, whether a titlebar is wanted in itself.
|
||||
//
|
||||
// Removing the titlebar also removes the entire window frame (ie. the ability to
|
||||
// close, move and resize the window [snapping still works]). This needs additional
|
||||
// handling in Zed, in order to implement drag handlers on a titlebar element.
|
||||
//
|
||||
// Since all of this handling is not present, we request server-side decorations
|
||||
// for now as a stopgap solution.
|
||||
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ServerSide);
|
||||
}
|
||||
|
||||
let viewport = state
|
||||
.viewporter
|
||||
.as_ref()
|
||||
@@ -210,6 +239,17 @@ impl Dispatch<wl_registry::WlRegistry, ()> for WaylandClientState {
|
||||
registry.bind::<wp_viewporter::WpViewporter, _, _>(name, 1, qh, ());
|
||||
state.viewporter = Some(view_porter);
|
||||
}
|
||||
"zxdg_decoration_manager_v1" => {
|
||||
// Unstable and optional
|
||||
let decoration_manager = registry
|
||||
.bind::<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, _, _>(
|
||||
name,
|
||||
1,
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
state.decoration_manager = Some(decoration_manager);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -222,6 +262,7 @@ delegate_noop!(WaylandClientState: ignore wl_shm::WlShm);
|
||||
delegate_noop!(WaylandClientState: ignore wl_shm_pool::WlShmPool);
|
||||
delegate_noop!(WaylandClientState: ignore wl_buffer::WlBuffer);
|
||||
delegate_noop!(WaylandClientState: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
|
||||
delegate_noop!(WaylandClientState: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
|
||||
delegate_noop!(WaylandClientState: ignore wp_viewporter::WpViewporter);
|
||||
delegate_noop!(WaylandClientState: ignore wp_viewport::WpViewport);
|
||||
|
||||
@@ -380,12 +421,29 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
|
||||
state.keymap_state = Some(xkb::State::new(&keymap));
|
||||
}
|
||||
wl_keyboard::Event::Enter { surface, .. } => {
|
||||
for window in &state.windows {
|
||||
if window.1.surface.id() == surface.id() {
|
||||
state.keyboard_focused_window = Some(Rc::clone(&window.1));
|
||||
}
|
||||
state.keyboard_focused_window = state
|
||||
.windows
|
||||
.iter()
|
||||
.find(|&w| w.1.surface.id() == surface.id())
|
||||
.map(|w| w.1.clone());
|
||||
|
||||
if let Some(window) = &state.keyboard_focused_window {
|
||||
window.set_focused(true);
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::Leave { surface, .. } => {
|
||||
let keyboard_focused_window = state
|
||||
.windows
|
||||
.iter()
|
||||
.find(|&w| w.1.surface.id() == surface.id())
|
||||
.map(|w| w.1.clone());
|
||||
|
||||
if let Some(window) = keyboard_focused_window {
|
||||
window.set_focused(false);
|
||||
}
|
||||
|
||||
state.keyboard_focused_window = None;
|
||||
}
|
||||
wl_keyboard::Event::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
@@ -411,55 +469,56 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
|
||||
} => {
|
||||
let keymap_state = state.keymap_state.as_ref().unwrap();
|
||||
let keycode = Keycode::from(key + MIN_KEYCODE);
|
||||
let key_utf32 = keymap_state.key_get_utf32(keycode);
|
||||
let key_utf8 = keymap_state.key_get_utf8(keycode);
|
||||
let key_sym = keymap_state.key_get_one_sym(keycode);
|
||||
let key = xkb::keysym_get_name(key_sym).to_lowercase();
|
||||
|
||||
// Ignore control characters (and DEL) for the purposes of ime_key,
|
||||
// but if key_utf32 is 0 then assume it isn't one
|
||||
let ime_key =
|
||||
(key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127)).then_some(key_utf8);
|
||||
|
||||
let focused_window = &state.keyboard_focused_window;
|
||||
if let Some(focused_window) = focused_window {
|
||||
match key_state {
|
||||
wl_keyboard::KeyState::Pressed => {
|
||||
focused_window.handle_input(PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: Keystroke {
|
||||
modifiers: state.modifiers,
|
||||
key,
|
||||
ime_key,
|
||||
},
|
||||
keystroke: Keystroke::from_xkb(
|
||||
keymap_state,
|
||||
state.modifiers,
|
||||
keycode,
|
||||
),
|
||||
is_held: false, // todo!(linux)
|
||||
}));
|
||||
}
|
||||
wl_keyboard::KeyState::Released => {
|
||||
focused_window.handle_input(PlatformInput::KeyUp(KeyUpEvent {
|
||||
keystroke: Keystroke {
|
||||
modifiers: state.modifiers,
|
||||
key,
|
||||
ime_key,
|
||||
},
|
||||
keystroke: Keystroke::from_xkb(
|
||||
keymap_state,
|
||||
state.modifiers,
|
||||
keycode,
|
||||
),
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::Leave { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn linux_button_to_gpui(button: u32) -> MouseButton {
|
||||
match button {
|
||||
0x110 => MouseButton::Left,
|
||||
0x111 => MouseButton::Right,
|
||||
0x112 => MouseButton::Middle,
|
||||
_ => unimplemented!(), // todo!(linux)
|
||||
}
|
||||
fn linux_button_to_gpui(button: u32) -> Option<MouseButton> {
|
||||
// These values are coming from <linux/input-event-codes.h>.
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
const BTN_RIGHT: u32 = 0x111;
|
||||
const BTN_MIDDLE: u32 = 0x112;
|
||||
const BTN_SIDE: u32 = 0x113;
|
||||
const BTN_EXTRA: u32 = 0x114;
|
||||
const BTN_FORWARD: u32 = 0x115;
|
||||
const BTN_BACK: u32 = 0x116;
|
||||
|
||||
Some(match button {
|
||||
BTN_LEFT => MouseButton::Left,
|
||||
BTN_RIGHT => MouseButton::Right,
|
||||
BTN_MIDDLE => MouseButton::Middle,
|
||||
BTN_BACK | BTN_SIDE => MouseButton::Navigate(NavigationDirection::Back),
|
||||
BTN_FORWARD | BTN_EXTRA => MouseButton::Navigate(NavigationDirection::Forward),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientState {
|
||||
@@ -513,16 +572,17 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientState {
|
||||
..
|
||||
} => {
|
||||
let focused_window = &state.mouse_focused_window;
|
||||
let mouse_location = &state.mouse_location;
|
||||
if let (Some(focused_window), Some(mouse_location)) =
|
||||
(focused_window, mouse_location)
|
||||
let mouse_location = state.mouse_location;
|
||||
let button = linux_button_to_gpui(button);
|
||||
if let (Some(focused_window), Some(mouse_location), Some(button)) =
|
||||
(focused_window, mouse_location, button)
|
||||
{
|
||||
match button_state {
|
||||
wl_pointer::ButtonState::Pressed => {
|
||||
state.button_pressed = Some(linux_button_to_gpui(button));
|
||||
state.button_pressed = Some(button);
|
||||
focused_window.handle_input(PlatformInput::MouseDown(MouseDownEvent {
|
||||
button: linux_button_to_gpui(button),
|
||||
position: *mouse_location,
|
||||
button,
|
||||
position: mouse_location,
|
||||
modifiers: state.modifiers,
|
||||
click_count: 1,
|
||||
}));
|
||||
@@ -530,8 +590,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientState {
|
||||
wl_pointer::ButtonState::Released => {
|
||||
state.button_pressed = None;
|
||||
focused_window.handle_input(PlatformInput::MouseUp(MouseUpEvent {
|
||||
button: linux_button_to_gpui(button),
|
||||
position: *mouse_location,
|
||||
button,
|
||||
position: mouse_location,
|
||||
modifiers: Modifiers::default(),
|
||||
click_count: 1,
|
||||
}));
|
||||
@@ -614,3 +674,42 @@ impl Dispatch<wp_fractional_scale_v1::WpFractionalScaleV1, ObjectId> for Wayland
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, ObjectId>
|
||||
for WaylandClientState
|
||||
{
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1,
|
||||
event: zxdg_toplevel_decoration_v1::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if let zxdg_toplevel_decoration_v1::Event::Configure { mode, .. } = event {
|
||||
for window in &state.windows {
|
||||
if window.0.id() == *surface_id {
|
||||
match mode {
|
||||
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
|
||||
window
|
||||
.1
|
||||
.set_decoration_state(WaylandDecorationState::Server);
|
||||
}
|
||||
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => {
|
||||
window
|
||||
.1
|
||||
.set_decoration_state(WaylandDecorationState::Client);
|
||||
}
|
||||
WEnum::Value(_) => {
|
||||
log::warn!("Unknown decoration mode");
|
||||
}
|
||||
WEnum::Unknown(v) => {
|
||||
log::warn!("Unknown decoration mode: {}", v);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ struct WaylandWindowInner {
|
||||
bounds: Bounds<i32>,
|
||||
scale: f32,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
decoration_state: WaylandDecorationState,
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
@@ -96,6 +97,9 @@ impl WaylandWindowInner {
|
||||
bounds,
|
||||
scale: 1.0,
|
||||
input_handler: None,
|
||||
|
||||
// On wayland, decorations are by default provided by the client
|
||||
decoration_state: WaylandDecorationState::Client,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +191,20 @@ impl WaylandWindowState {
|
||||
self.set_size_and_scale(bounds.size.width, bounds.size.height, scale)
|
||||
}
|
||||
|
||||
/// Notifies the window of the state of the decorations.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This API is indirectly called by the wayland compositor and
|
||||
/// not meant to be called by a user who wishes to change the state
|
||||
/// of the decorations. This is because the state of the decorations
|
||||
/// is managed by the compositor and not the client.
|
||||
pub fn set_decoration_state(&self, state: WaylandDecorationState) {
|
||||
self.inner.lock().decoration_state = state;
|
||||
log::trace!("Window decorations are now handled by {:?}", state);
|
||||
// todo!(linux) - Handle this properly
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let mut callbacks = self.callbacks.lock();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
@@ -210,6 +228,12 @@ impl WaylandWindowState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_focused(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.lock().active_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -331,7 +355,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
//todo!(linux)
|
||||
self.0.callbacks.lock().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
@@ -377,3 +401,12 @@ impl PlatformWindow for WaylandWindow {
|
||||
//todo!(linux)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum WaylandDecorationState {
|
||||
/// Decorations are to be provided by the client
|
||||
Client,
|
||||
|
||||
/// Decorations are provided by the server
|
||||
Server,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ use crate::platform::{
|
||||
LinuxPlatformInner, PlatformWindow, X11Display, X11Window, X11WindowState, XcbAtoms,
|
||||
};
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, DisplayId, PlatformDisplay, PlatformInput, Point, Size, WindowOptions,
|
||||
AnyWindowHandle, Bounds, DisplayId, PlatformDisplay, PlatformInput, Point, ScrollDelta, Size,
|
||||
TouchPhase, WindowOptions,
|
||||
};
|
||||
|
||||
pub(crate) struct X11ClientState {
|
||||
@@ -70,7 +71,10 @@ impl Client for X11Client {
|
||||
// into window functions as they may invoke callbacks that need
|
||||
// to immediately access the platform (self).
|
||||
while !self.platform_inner.state.lock().quit_requested {
|
||||
let event = self.xcb_connection.wait_for_event().unwrap();
|
||||
let event = {
|
||||
profiling::scope!("Wait for event");
|
||||
self.xcb_connection.wait_for_event().unwrap()
|
||||
};
|
||||
match event {
|
||||
xcb::Event::X(x::Event::ClientMessage(ev)) => {
|
||||
if let x::ClientMessageData::Data32([atom, ..]) = ev.data() {
|
||||
@@ -106,42 +110,42 @@ impl Client for X11Client {
|
||||
window.request_refresh();
|
||||
}
|
||||
xcb::Event::Present(xcb::present::Event::IdleNotify(_ev)) => {}
|
||||
xcb::Event::X(x::Event::FocusIn(ev)) => {
|
||||
let window = self.get_window(ev.event());
|
||||
window.set_focused(true);
|
||||
}
|
||||
xcb::Event::X(x::Event::FocusOut(ev)) => {
|
||||
let window = self.get_window(ev.event());
|
||||
window.set_focused(false);
|
||||
}
|
||||
xcb::Event::X(x::Event::KeyPress(ev)) => {
|
||||
let window = self.get_window(ev.event());
|
||||
let modifiers = super::modifiers_from_state(ev.state());
|
||||
let key = {
|
||||
let keystroke = {
|
||||
let code = ev.detail().into();
|
||||
let mut state = self.state.lock();
|
||||
let key = state.xkb.key_get_utf8(code);
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
state.xkb.update_key(code, xkb::KeyDirection::Down);
|
||||
key
|
||||
keystroke
|
||||
};
|
||||
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke: crate::Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
ime_key: None,
|
||||
},
|
||||
keystroke,
|
||||
is_held: false,
|
||||
}));
|
||||
}
|
||||
xcb::Event::X(x::Event::KeyRelease(ev)) => {
|
||||
let window = self.get_window(ev.event());
|
||||
let modifiers = super::modifiers_from_state(ev.state());
|
||||
let key = {
|
||||
let keystroke = {
|
||||
let code = ev.detail().into();
|
||||
let mut state = self.state.lock();
|
||||
let key = state.xkb.key_get_utf8(code);
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
state.xkb.update_key(code, xkb::KeyDirection::Up);
|
||||
key
|
||||
keystroke
|
||||
};
|
||||
window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent {
|
||||
keystroke: crate::Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
ime_key: None,
|
||||
},
|
||||
}));
|
||||
|
||||
window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { keystroke }));
|
||||
}
|
||||
xcb::Event::X(x::Event::ButtonPress(ev)) => {
|
||||
let window = self.get_window(ev.event());
|
||||
@@ -155,6 +159,15 @@ impl Client for X11Client {
|
||||
modifiers,
|
||||
click_count: 1,
|
||||
}));
|
||||
} else if ev.detail() >= 4 && ev.detail() <= 5 {
|
||||
// https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11
|
||||
let delta_x = if ev.detail() == 4 { 1.0 } else { -1.0 };
|
||||
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(0.0, delta_x)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
}));
|
||||
} else {
|
||||
log::warn!("Unknown button press: {ev:?}");
|
||||
}
|
||||
@@ -200,6 +213,7 @@ impl Client for X11Client {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
profiling::scope!("Runnables");
|
||||
if let Ok(runnable) = self.platform_inner.main_receiver.try_recv() {
|
||||
runnable.run();
|
||||
}
|
||||
@@ -209,6 +223,7 @@ impl Client for X11Client {
|
||||
fun();
|
||||
}
|
||||
}
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
let setup = self.xcb_connection.get_setup();
|
||||
setup
|
||||
@@ -220,6 +235,7 @@ impl Client for X11Client {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(Rc::new(X11Display::new(&self.xcb_connection, id.0 as i32)))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use xcb::x;
|
||||
|
||||
use crate::{Modifiers, MouseButton};
|
||||
use crate::{Modifiers, MouseButton, NavigationDirection};
|
||||
|
||||
pub(crate) fn button_of_key(detail: x::Button) -> Option<MouseButton> {
|
||||
Some(match detail {
|
||||
1 => MouseButton::Left,
|
||||
2 => MouseButton::Middle,
|
||||
3 => MouseButton::Right,
|
||||
8 => MouseButton::Navigate(NavigationDirection::Back),
|
||||
9 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use crate::{
|
||||
platform::blade::BladeRenderer, size, Bounds, GlobalPixels, Pixels, PlatformDisplay,
|
||||
PlatformInput, PlatformInputHandler, PlatformWindow, Point, Size, WindowAppearance,
|
||||
WindowBounds, WindowOptions, X11Display,
|
||||
platform::blade::BladeRenderer, size, Bounds, GlobalPixels, Modifiers, Pixels, PlatformAtlas,
|
||||
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
|
||||
Scene, Size, WindowAppearance, WindowBounds, WindowOptions, X11Display,
|
||||
};
|
||||
use blade_graphics as gpu;
|
||||
use parking_lot::Mutex;
|
||||
@@ -27,7 +27,7 @@ use std::{
|
||||
#[derive(Default)]
|
||||
struct Callbacks {
|
||||
request_frame: Option<Box<dyn FnMut()>>,
|
||||
input: Option<Box<dyn FnMut(crate::PlatformInput) -> bool>>,
|
||||
input: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
|
||||
active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
fullscreen: Option<Box<dyn FnMut(bool)>>,
|
||||
@@ -155,6 +155,9 @@ impl X11WindowState {
|
||||
x::Cw::EventMask(
|
||||
x::EventMask::EXPOSURE
|
||||
| x::EventMask::STRUCTURE_NOTIFY
|
||||
| x::EventMask::ENTER_WINDOW
|
||||
| x::EventMask::LEAVE_WINDOW
|
||||
| x::EventMask::FOCUS_CHANGE
|
||||
| x::EventMask::KEY_PRESS
|
||||
| x::EventMask::KEY_RELEASE
|
||||
| x::EventMask::BUTTON_PRESS
|
||||
@@ -205,25 +208,21 @@ impl X11WindowState {
|
||||
});
|
||||
}
|
||||
}
|
||||
xcb_connection
|
||||
.send_and_check_request(&x::ChangeProperty {
|
||||
mode: x::PropMode::Replace,
|
||||
window: x_window,
|
||||
property: atoms.wm_protocols,
|
||||
r#type: x::ATOM_ATOM,
|
||||
data: &[atoms.wm_del_window],
|
||||
})
|
||||
.unwrap();
|
||||
xcb_connection.send_request(&x::ChangeProperty {
|
||||
mode: x::PropMode::Replace,
|
||||
window: x_window,
|
||||
property: atoms.wm_protocols,
|
||||
r#type: x::ATOM_ATOM,
|
||||
data: &[atoms.wm_del_window],
|
||||
});
|
||||
|
||||
let fake_id = xcb_connection.generate_id();
|
||||
xcb_connection
|
||||
.send_and_check_request(&xcb::present::SelectInput {
|
||||
eid: fake_id,
|
||||
window: x_window,
|
||||
//Note: also consider `IDLE_NOTIFY`
|
||||
event_mask: xcb::present::EventMask::COMPLETE_NOTIFY,
|
||||
})
|
||||
.unwrap();
|
||||
xcb_connection.send_request(&xcb::present::SelectInput {
|
||||
eid: fake_id,
|
||||
window: x_window,
|
||||
//Note: also consider `IDLE_NOTIFY`
|
||||
event_mask: xcb::present::EventMask::COMPLETE_NOTIFY,
|
||||
});
|
||||
|
||||
xcb_connection.send_request(&x::MapWindow { window: x_window });
|
||||
xcb_connection.flush().unwrap();
|
||||
@@ -241,7 +240,7 @@ impl X11WindowState {
|
||||
gpu::Context::init_windowed(
|
||||
&raw,
|
||||
gpu::ContextDesc {
|
||||
validation: cfg!(debug_assertions),
|
||||
validation: false,
|
||||
capture: false,
|
||||
},
|
||||
)
|
||||
@@ -321,15 +320,13 @@ impl X11WindowState {
|
||||
}
|
||||
|
||||
pub fn request_refresh(&self) {
|
||||
self.xcb_connection
|
||||
.send_and_check_request(&xcb::present::NotifyMsc {
|
||||
window: self.x_window,
|
||||
serial: 0,
|
||||
target_msc: 0,
|
||||
divisor: 1,
|
||||
remainder: 0,
|
||||
})
|
||||
.unwrap();
|
||||
self.xcb_connection.send_request(&xcb::present::NotifyMsc {
|
||||
window: self.x_window,
|
||||
serial: 0,
|
||||
target_msc: 0,
|
||||
divisor: 1,
|
||||
remainder: 0,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
@@ -341,10 +338,18 @@ impl X11WindowState {
|
||||
if let PlatformInput::KeyDown(event) = input {
|
||||
let mut inner = self.inner.lock();
|
||||
if let Some(ref mut input_handler) = inner.input_handler {
|
||||
input_handler.replace_text_in_range(None, &event.keystroke.key);
|
||||
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||
input_handler.replace_text_in_range(None, ime_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_focused(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.lock().active_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
@@ -374,14 +379,20 @@ impl PlatformWindow for X11Window {
|
||||
Rc::clone(&self.0.display)
|
||||
}
|
||||
|
||||
//todo!(linux)
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
Point::default()
|
||||
let cookie = self.0.xcb_connection.send_request(&x::QueryPointer {
|
||||
window: self.0.x_window,
|
||||
});
|
||||
let reply: x::QueryPointerReply = self.0.xcb_connection.wait_for_reply(cookie).unwrap();
|
||||
Point::new(
|
||||
(reply.root_x() as u32).into(),
|
||||
(reply.root_y() as u32).into(),
|
||||
)
|
||||
}
|
||||
|
||||
//todo!(linux)
|
||||
fn modifiers(&self) -> crate::Modifiers {
|
||||
crate::Modifiers::default()
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
Modifiers::default()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
@@ -399,7 +410,7 @@ impl PlatformWindow for X11Window {
|
||||
//todo!(linux)
|
||||
fn prompt(
|
||||
&self,
|
||||
_level: crate::PromptLevel,
|
||||
_level: PromptLevel,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
@@ -456,7 +467,7 @@ impl PlatformWindow for X11Window {
|
||||
self.0.callbacks.lock().request_frame = Some(callback);
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(crate::PlatformInput) -> bool>) {
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
|
||||
self.0.callbacks.lock().input = Some(callback);
|
||||
}
|
||||
|
||||
@@ -489,16 +500,16 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
//todo!(linux)
|
||||
fn is_topmost_for_position(&self, _position: crate::Point<Pixels>) -> bool {
|
||||
fn is_topmost_for_position(&self, _position: Point<Pixels>) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &crate::Scene) {
|
||||
fn draw(&self, scene: &Scene) {
|
||||
let mut inner = self.0.inner.lock();
|
||||
inner.renderer.draw(scene);
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> {
|
||||
let inner = self.0.inner.lock();
|
||||
inner.renderer.sprite_atlas().clone()
|
||||
}
|
||||
|
||||
@@ -125,6 +125,9 @@ impl Platform for TestPlatform {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return Arc::new(crate::platform::mac::MacTextSystem::new());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
todo!("windows")
|
||||
}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
|
||||
Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
|
||||
px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, Pixels, PlatformAtlas,
|
||||
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, Size,
|
||||
TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
@@ -112,41 +112,6 @@ impl TestWindow {
|
||||
self.0.lock().input_callback = Some(callback);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn simulate_keystroke(&mut self, mut keystroke: Keystroke, is_held: bool) {
|
||||
if keystroke.ime_key.is_none()
|
||||
&& !keystroke.modifiers.command
|
||||
&& !keystroke.modifiers.control
|
||||
&& !keystroke.modifiers.function
|
||||
{
|
||||
keystroke.ime_key = Some(if keystroke.modifiers.shift {
|
||||
keystroke.key.to_ascii_uppercase().clone()
|
||||
} else {
|
||||
keystroke.key.clone()
|
||||
})
|
||||
}
|
||||
|
||||
if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke.clone(),
|
||||
is_held,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut lock = self.0.lock();
|
||||
let Some(mut input_handler) = lock.input_handler.take() else {
|
||||
panic!(
|
||||
"simulate_keystroke {:?} input event was not handled and there was no active input",
|
||||
&keystroke
|
||||
);
|
||||
};
|
||||
drop(lock);
|
||||
if let Some(text) = keystroke.ime_key.as_ref() {
|
||||
input_handler.replace_text_in_range(None, &text);
|
||||
}
|
||||
|
||||
self.0.lock().input_handler = Some(input_handler);
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for TestWindow {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
345
crates/gpui/src/scene/primitives.rs
Normal file
345
crates/gpui/src/scene/primitives.rs
Normal file
@@ -0,0 +1,345 @@
|
||||
use crate::{
|
||||
point, AtlasTile, Bounds, ContentMask, Corners, Edges, EntityId, Hsla, Pixels, Point,
|
||||
ScaledPixels,
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Default, Debug, Clone, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct Quad {
|
||||
pub view_id: ViewId,
|
||||
pub order: u32,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub background: Hsla,
|
||||
pub border_color: Hsla,
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
pub border_widths: Edges<ScaledPixels>,
|
||||
}
|
||||
|
||||
impl Ord for Quad {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Quad {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct Underline {
|
||||
pub view_id: ViewId,
|
||||
pub order: u32,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
pub thickness: ScaledPixels,
|
||||
pub wavy: bool,
|
||||
}
|
||||
|
||||
impl Ord for Underline {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Underline {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct Shadow {
|
||||
pub view_id: ViewId,
|
||||
pub order: u32,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
pub blur_radius: ScaledPixels,
|
||||
pub pad: u32, // align to 8 bytes
|
||||
}
|
||||
|
||||
impl Ord for Shadow {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Shadow {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct MonochromeSprite {
|
||||
pub view_id: ViewId,
|
||||
pub order: u32,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
pub tile: AtlasTile,
|
||||
}
|
||||
|
||||
impl Ord for MonochromeSprite {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.order.cmp(&other.order) {
|
||||
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
|
||||
order => order,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for MonochromeSprite {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct PolychromeSprite {
|
||||
pub view_id: ViewId,
|
||||
pub order: u32,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
pub tile: AtlasTile,
|
||||
pub grayscale: bool,
|
||||
pub pad: u32, // align to 8 bytes
|
||||
}
|
||||
|
||||
impl Ord for PolychromeSprite {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.order.cmp(&other.order) {
|
||||
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
|
||||
order => order,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PolychromeSprite {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct Surface {
|
||||
pub view_id: ViewId,
|
||||
pub order: u32,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub image_buffer: media::core_video::CVImageBuffer,
|
||||
}
|
||||
|
||||
impl Ord for Surface {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Surface {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct PathId(pub(crate) usize);
|
||||
|
||||
/// A line made up of a series of vertices and control points.
|
||||
#[derive(Debug)]
|
||||
pub struct Path<P: Clone + Default + Debug> {
|
||||
pub(crate) id: PathId,
|
||||
pub(crate) view_id: ViewId,
|
||||
pub(crate) order: u32,
|
||||
pub(crate) bounds: Bounds<P>,
|
||||
pub(crate) content_mask: ContentMask<P>,
|
||||
pub(crate) vertices: Vec<PathVertex<P>>,
|
||||
pub(crate) color: Hsla,
|
||||
pub(crate) start: Point<P>,
|
||||
pub(crate) current: Point<P>,
|
||||
pub(crate) contour_count: usize,
|
||||
}
|
||||
|
||||
impl Path<Pixels> {
|
||||
/// Create a new path with the given starting point.
|
||||
pub fn new(start: Point<Pixels>) -> Self {
|
||||
Self {
|
||||
id: PathId(0),
|
||||
view_id: ViewId::default(),
|
||||
order: u32::default(),
|
||||
vertices: Vec::new(),
|
||||
start,
|
||||
current: start,
|
||||
bounds: Bounds {
|
||||
origin: start,
|
||||
size: Default::default(),
|
||||
},
|
||||
content_mask: Default::default(),
|
||||
color: Default::default(),
|
||||
contour_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale this path by the given factor.
|
||||
pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
|
||||
Path {
|
||||
id: self.id,
|
||||
view_id: self.view_id,
|
||||
order: self.order,
|
||||
bounds: self.bounds.scale(factor),
|
||||
content_mask: self.content_mask.scale(factor),
|
||||
vertices: self
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| vertex.scale(factor))
|
||||
.collect(),
|
||||
start: self.start.map(|start| start.scale(factor)),
|
||||
current: self.current.scale(factor),
|
||||
contour_count: self.contour_count,
|
||||
color: self.color,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a straight line from the current point to the given point.
|
||||
pub fn line_to(&mut self, to: Point<Pixels>) {
|
||||
self.contour_count += 1;
|
||||
if self.contour_count > 1 {
|
||||
self.push_triangle(
|
||||
(self.start, self.current, to),
|
||||
(point(0., 1.), point(0., 1.), point(0., 1.)),
|
||||
);
|
||||
}
|
||||
self.current = to;
|
||||
}
|
||||
|
||||
/// Draw a curve from the current point to the given point, using the given control point.
|
||||
pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
|
||||
self.contour_count += 1;
|
||||
if self.contour_count > 1 {
|
||||
self.push_triangle(
|
||||
(self.start, self.current, to),
|
||||
(point(0., 1.), point(0., 1.), point(0., 1.)),
|
||||
);
|
||||
}
|
||||
|
||||
self.push_triangle(
|
||||
(self.current, ctrl, to),
|
||||
(point(0., 0.), point(0.5, 0.), point(1., 1.)),
|
||||
);
|
||||
self.current = to;
|
||||
}
|
||||
|
||||
fn push_triangle(
|
||||
&mut self,
|
||||
xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>),
|
||||
st: (Point<f32>, Point<f32>, Point<f32>),
|
||||
) {
|
||||
self.bounds = self
|
||||
.bounds
|
||||
.union(&Bounds {
|
||||
origin: xy.0,
|
||||
size: Default::default(),
|
||||
})
|
||||
.union(&Bounds {
|
||||
origin: xy.1,
|
||||
size: Default::default(),
|
||||
})
|
||||
.union(&Bounds {
|
||||
origin: xy.2,
|
||||
size: Default::default(),
|
||||
});
|
||||
|
||||
self.vertices.push(PathVertex {
|
||||
xy_position: xy.0,
|
||||
st_position: st.0,
|
||||
content_mask: Default::default(),
|
||||
});
|
||||
self.vertices.push(PathVertex {
|
||||
xy_position: xy.1,
|
||||
st_position: st.1,
|
||||
content_mask: Default::default(),
|
||||
});
|
||||
self.vertices.push(PathVertex {
|
||||
xy_position: xy.2,
|
||||
st_position: st.2,
|
||||
content_mask: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Path<ScaledPixels> {}
|
||||
|
||||
impl PartialEq for Path<ScaledPixels> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.order == other.order
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Path<ScaledPixels> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Path<ScaledPixels> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct PathVertex<P: Clone + Default + Debug> {
|
||||
pub(crate) xy_position: Point<P>,
|
||||
pub(crate) st_position: Point<f32>,
|
||||
pub(crate) content_mask: ContentMask<P>,
|
||||
}
|
||||
|
||||
impl PathVertex<Pixels> {
|
||||
pub fn scale(&self, factor: f32) -> PathVertex<ScaledPixels> {
|
||||
PathVertex {
|
||||
xy_position: self.xy_position.scale(factor),
|
||||
st_position: self.st_position,
|
||||
content_mask: self.content_mask.scale(factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types, unused)]
|
||||
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct ViewId {
|
||||
low_bits: u32,
|
||||
high_bits: u32,
|
||||
}
|
||||
|
||||
impl From<EntityId> for ViewId {
|
||||
fn from(value: EntityId) -> Self {
|
||||
let value = value.as_u64();
|
||||
Self {
|
||||
low_bits: value as u32,
|
||||
high_bits: (value >> 32) as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ViewId> for EntityId {
|
||||
fn from(value: ViewId) -> Self {
|
||||
let value = (value.low_bits as u64) | ((value.high_bits as u64) << 32);
|
||||
value.into()
|
||||
}
|
||||
}
|
||||
@@ -383,10 +383,12 @@ impl Style {
|
||||
}
|
||||
}
|
||||
|
||||
/// Paints the background of an element styled with this style.
|
||||
/// Paints the background of an element styled with this style, then calls the continuation function, then paints the border.
|
||||
pub fn paint(
|
||||
&self,
|
||||
bounds: Bounds<Pixels>,
|
||||
hover: Option<Self>,
|
||||
group_hover: Option<(SharedString, Option<Self>)>,
|
||||
cx: &mut ElementContext,
|
||||
continuation: impl FnOnce(&mut ElementContext),
|
||||
) {
|
||||
@@ -397,7 +399,7 @@ impl Style {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug || cx.has_global::<DebugBelow>() {
|
||||
cx.paint_quad(crate::outline(bounds, crate::red()));
|
||||
cx.paint_quad(crate::outline(bounds, crate::red()), None, None);
|
||||
}
|
||||
|
||||
let rem_size = cx.rem_size();
|
||||
@@ -410,101 +412,229 @@ impl Style {
|
||||
);
|
||||
});
|
||||
|
||||
let background_color = self.background.as_ref().and_then(Fill::color);
|
||||
if background_color.map_or(false, |color| !color.is_transparent()) {
|
||||
cx.with_z_index(1, |cx| {
|
||||
let mut border_color = background_color.unwrap_or_default();
|
||||
border_color.a = 0.;
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
self.corner_radii.to_pixels(bounds.size, rem_size),
|
||||
background_color.unwrap_or_default(),
|
||||
Edges::default(),
|
||||
border_color,
|
||||
));
|
||||
});
|
||||
}
|
||||
let named_hover_group = group_hover
|
||||
.as_ref()
|
||||
.map(|(group_name, _)| group_name.clone());
|
||||
cx.with_hover_group(named_hover_group, |cx| {
|
||||
let background_color = self.background_color();
|
||||
let hover_background_color = hover
|
||||
.as_ref()
|
||||
.map(|hover_style| hover_style.background_color())
|
||||
.unwrap_or_default();
|
||||
let group_hover_background_color = group_hover
|
||||
.as_ref()
|
||||
.and_then(|(_, group_hover_style)| {
|
||||
Some(group_hover_style.as_ref()?.background_color())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
cx.with_z_index(2, |cx| {
|
||||
continuation(cx);
|
||||
});
|
||||
if !background_color.is_transparent()
|
||||
|| !hover_background_color.is_transparent()
|
||||
|| !group_hover_background_color.is_transparent()
|
||||
{
|
||||
cx.with_z_index(1, |cx| {
|
||||
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
|
||||
|
||||
if self.is_border_visible() {
|
||||
cx.with_z_index(3, |cx| {
|
||||
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
|
||||
let border_widths = self.border_widths.to_pixels(rem_size);
|
||||
let max_border_width = border_widths.max();
|
||||
let max_corner_radius = corner_radii.max();
|
||||
let mut border_color = background_color;
|
||||
border_color.a = 0.;
|
||||
let base_quad = quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
background_color,
|
||||
Edges::default(),
|
||||
border_color,
|
||||
);
|
||||
|
||||
let top_bounds = Bounds::from_corners(
|
||||
bounds.origin,
|
||||
bounds.upper_right()
|
||||
+ point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
|
||||
);
|
||||
let bottom_bounds = Bounds::from_corners(
|
||||
bounds.lower_left()
|
||||
- point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
|
||||
bounds.lower_right(),
|
||||
);
|
||||
let left_bounds = Bounds::from_corners(
|
||||
top_bounds.lower_left(),
|
||||
bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
|
||||
);
|
||||
let right_bounds = Bounds::from_corners(
|
||||
top_bounds.lower_right() - point(max_border_width, Pixels::ZERO),
|
||||
bottom_bounds.upper_right(),
|
||||
);
|
||||
let hover_quad = if hover_background_color.is_transparent() {
|
||||
None
|
||||
} else {
|
||||
let mut border_color = hover_background_color;
|
||||
border_color.a = 0.;
|
||||
Some(quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
hover_background_color,
|
||||
Edges::default(),
|
||||
border_color,
|
||||
))
|
||||
};
|
||||
|
||||
let mut background = self.border_color.unwrap_or_default();
|
||||
background.a = 0.;
|
||||
let quad = quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
background,
|
||||
border_widths,
|
||||
self.border_color.unwrap_or_default(),
|
||||
);
|
||||
let group_hover_quad = group_hover.as_ref().map(|(group_id, _)| {
|
||||
let quad = if group_hover_background_color.is_transparent() {
|
||||
None
|
||||
} else {
|
||||
let mut border_color = group_hover_background_color;
|
||||
border_color.a = 0.;
|
||||
Some(quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
group_hover_background_color,
|
||||
Edges::default(),
|
||||
border_color,
|
||||
))
|
||||
};
|
||||
(group_id.clone(), quad)
|
||||
});
|
||||
|
||||
cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
cx.paint_quad(base_quad, hover_quad, group_hover_quad);
|
||||
});
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: right_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
},
|
||||
);
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: bottom_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
},
|
||||
);
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: left_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug_below {
|
||||
cx.remove_global::<DebugBelow>();
|
||||
cx.with_z_index(2, |cx| {
|
||||
continuation(cx);
|
||||
});
|
||||
|
||||
let border_color = self.border_color();
|
||||
let hover_border_color = hover
|
||||
.as_ref()
|
||||
.map(|hover_style| hover_style.border_color())
|
||||
.unwrap_or_default();
|
||||
let group_hover_border_color = group_hover
|
||||
.as_ref()
|
||||
.and_then(|(_, group_hover_style)| Some(group_hover_style.as_ref()?.border_color()))
|
||||
.unwrap_or_default();
|
||||
if self.border_widths.any(|width| !width.is_zero())
|
||||
&& (!border_color.is_transparent()
|
||||
|| !hover_border_color.is_transparent()
|
||||
|| !group_hover_border_color.is_transparent())
|
||||
{
|
||||
cx.with_z_index(3, |cx| {
|
||||
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
|
||||
let border_widths = self.border_widths.to_pixels(rem_size);
|
||||
let max_border_width = border_widths.max();
|
||||
let max_corner_radius = corner_radii.max();
|
||||
|
||||
let top_bounds = Bounds::from_corners(
|
||||
bounds.origin,
|
||||
bounds.upper_right()
|
||||
+ point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
|
||||
);
|
||||
let bottom_bounds = Bounds::from_corners(
|
||||
bounds.lower_left()
|
||||
- point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
|
||||
bounds.lower_right(),
|
||||
);
|
||||
let left_bounds = Bounds::from_corners(
|
||||
top_bounds.lower_left(),
|
||||
bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
|
||||
);
|
||||
let right_bounds = Bounds::from_corners(
|
||||
top_bounds.lower_right() - point(max_border_width, Pixels::ZERO),
|
||||
bottom_bounds.upper_right(),
|
||||
);
|
||||
|
||||
let mut background = border_color;
|
||||
background.a = 0.;
|
||||
let border_quad = quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
background,
|
||||
border_widths,
|
||||
border_color,
|
||||
);
|
||||
let hover_border_quad = if hover_border_color.is_transparent() {
|
||||
None
|
||||
} else {
|
||||
let mut background = hover_border_color;
|
||||
background.a = 0.;
|
||||
Some(quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
background,
|
||||
border_widths,
|
||||
hover_border_color,
|
||||
))
|
||||
};
|
||||
let group_hover_border_quad = group_hover.as_ref().map(|(group_id, _)| {
|
||||
let quad = if group_hover_border_color.is_transparent() {
|
||||
None
|
||||
} else {
|
||||
let mut background = group_hover_border_color;
|
||||
background.a = 0.;
|
||||
Some(quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
background,
|
||||
border_widths,
|
||||
group_hover_border_color,
|
||||
))
|
||||
};
|
||||
(group_id.clone(), quad)
|
||||
});
|
||||
|
||||
cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| {
|
||||
cx.paint_quad(
|
||||
border_quad.clone(),
|
||||
hover_border_quad.clone(),
|
||||
group_hover_border_quad.clone(),
|
||||
);
|
||||
});
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: right_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(
|
||||
border_quad.clone(),
|
||||
hover_border_quad.clone(),
|
||||
group_hover_border_quad.clone(),
|
||||
);
|
||||
},
|
||||
);
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: bottom_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(
|
||||
border_quad.clone(),
|
||||
hover_border_quad.clone(),
|
||||
group_hover_border_quad.clone(),
|
||||
);
|
||||
},
|
||||
);
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: left_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(border_quad, hover_border_quad, group_hover_border_quad);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug_below {
|
||||
cx.remove_global::<DebugBelow>();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the background color of the style based on the visibility.
|
||||
/// If the visibility is `Visible`, it returns the background color of the style if set,
|
||||
/// otherwise it returns the default color. If the visibility is `Hidden`, it returns
|
||||
/// a transparent black color.
|
||||
fn background_color(&self) -> Hsla {
|
||||
match self.visibility {
|
||||
Visibility::Visible => self
|
||||
.background
|
||||
.as_ref()
|
||||
.and_then(Fill::color)
|
||||
.unwrap_or_default(),
|
||||
Visibility::Hidden => Hsla::transparent_black(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_border_visible(&self) -> bool {
|
||||
self.border_color
|
||||
.map_or(false, |color| !color.is_transparent())
|
||||
&& self.border_widths.any(|length| !length.is_zero())
|
||||
/// Returns the border color of the style based on the visibility.
|
||||
/// If the visibility is `Visible`, it returns the border color of the style if set,
|
||||
/// otherwise it returns the default color. If the visibility is `Hidden`, it returns
|
||||
/// a transparent black color.
|
||||
fn border_color(&self) -> Hsla {
|
||||
match self.visibility {
|
||||
Visibility::Visible => self.border_color.unwrap_or_default(),
|
||||
Visibility::Hidden => Hsla::transparent_black(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user