Compare commits
108 Commits
another
...
lua-syntax
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6a8b4cfb1 | ||
|
|
264ac61210 | ||
|
|
ad4742a5b8 | ||
|
|
b0d4abb82e | ||
|
|
387ee46c46 | ||
|
|
89d89b8b2d | ||
|
|
f07ae541ad | ||
|
|
ff0bb1f389 | ||
|
|
9c7eee24bc | ||
|
|
ec4719146a | ||
|
|
47f8f891c8 | ||
|
|
d9d3b8847b | ||
|
|
d7b90f4204 | ||
|
|
3e64f38ba0 | ||
|
|
82338e2c47 | ||
|
|
229e853874 | ||
|
|
ed13e05855 | ||
|
|
674fb7621f | ||
|
|
fe18c73a07 | ||
|
|
befacfe8c9 | ||
|
|
54f0a729c2 | ||
|
|
67f9b2b87f | ||
|
|
a4ec0af681 | ||
|
|
886d8c1cab | ||
|
|
ebc5c213a2 | ||
|
|
0a2d938ac5 | ||
|
|
fc01f496a9 | ||
|
|
db28b9bbde | ||
|
|
0453cb2b06 | ||
|
|
85211889e5 | ||
|
|
ad94642e83 | ||
|
|
f4899d92a4 | ||
|
|
6685d85f49 | ||
|
|
161f8a1dd2 | ||
|
|
6cdd7b7390 | ||
|
|
0ec15d6b02 | ||
|
|
909de2ca6f | ||
|
|
f31749c81b | ||
|
|
76a81607de | ||
|
|
7d22059a2f | ||
|
|
6b16a5555e | ||
|
|
7ba2b258de | ||
|
|
cbb535f5eb | ||
|
|
20fc753f2b | ||
|
|
042fc82e99 | ||
|
|
27781a8a60 | ||
|
|
33af6bce55 | ||
|
|
563baf682e | ||
|
|
11b79d0ab9 | ||
|
|
8c4da9fba0 | ||
|
|
1f7fa80166 | ||
|
|
2ac952ee6b | ||
|
|
495612be2e | ||
|
|
1086b282b8 | ||
|
|
ffe2bed1e2 | ||
|
|
88940732ca | ||
|
|
74fc52d5ce | ||
|
|
2a7a4a80c6 | ||
|
|
c03bf1af36 | ||
|
|
922aaa0534 | ||
|
|
fc5ff318e3 | ||
|
|
e7b3b8bf03 | ||
|
|
6faa7cd722 | ||
|
|
0776fa8f31 | ||
|
|
7321c814ce | ||
|
|
ac3cb3df05 | ||
|
|
0bd40da546 | ||
|
|
3bec4eb117 | ||
|
|
bf6cc2697a | ||
|
|
dc3158c8ce | ||
|
|
9e2b7bc5dc | ||
|
|
b774a4b8d1 | ||
|
|
16ab8701a2 | ||
|
|
b2add8c803 | ||
|
|
6635462f7b | ||
|
|
81ff6f7a3c | ||
|
|
669082dbe0 | ||
|
|
d5bc7b9a79 | ||
|
|
8bb2739e28 | ||
|
|
466be14b56 | ||
|
|
95446195af | ||
|
|
b34c0fd71b | ||
|
|
e0060b92cc | ||
|
|
06bcc42652 | ||
|
|
f24c226af8 | ||
|
|
593f3dc1d5 | ||
|
|
61d584db45 | ||
|
|
c37f616c3b | ||
|
|
73ac19958a | ||
|
|
508b9d3b5d | ||
|
|
0a4ff2f475 | ||
|
|
ae6d350334 | ||
|
|
0e44f93178 | ||
|
|
65d92d7278 | ||
|
|
8b5ef2558b | ||
|
|
fec228bb23 | ||
|
|
e00d737196 | ||
|
|
b0dee94126 | ||
|
|
e65471c7a1 | ||
|
|
6713ec8cdf | ||
|
|
48e09c0026 | ||
|
|
3f03d7b023 | ||
|
|
fa96e2259b | ||
|
|
a8a05f208b | ||
|
|
aa1ab50656 | ||
|
|
d115cb1944 | ||
|
|
42571e405f | ||
|
|
2d61a51ded |
51
.github/ISSUE_TEMPLATE/0_git_beta_bug_report.yml
vendored
Normal file
51
.github/ISSUE_TEMPLATE/0_git_beta_bug_report.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Git Beta
|
||||
description: There is a bug related to new Git features in Zed
|
||||
type: "Bug"
|
||||
labels: [git]
|
||||
title: "Git Beta: <a short description of the Git bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the bug with a one line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
|
||||
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
|
||||
Steps to trigger the problem:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Actual Behavior:
|
||||
|
||||
Expected Behavior:
|
||||
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: Copy System Specs Into Clipboard"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
macOS: `~/Library/Logs/Zed/Zed.log`
|
||||
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary>
|
||||
|
||||
<!-- Click below this line and paste or drag-and-drop your log-->
|
||||
```
|
||||
|
||||
```
|
||||
<!-- Click above this line and paste or drag-and-drop your log--></details>
|
||||
validations:
|
||||
required: false
|
||||
224
Cargo.lock
generated
224
Cargo.lock
generated
@@ -644,9 +644,11 @@ dependencies = [
|
||||
"collections",
|
||||
"derive_more",
|
||||
"gpui",
|
||||
"language",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ui",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -1178,9 +1180,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.5.17"
|
||||
version = "1.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490aa7465ee685b2ced076bb87ef654a47724a7844e2c7d3af4e749ce5b875dd"
|
||||
checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1271,9 +1273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-bedrockruntime"
|
||||
version = "1.75.0"
|
||||
version = "1.74.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ddf7475b6f50a1a5be8edb1bcdf6e4ae00feed5b890d14a3f1f0e14d76f5a16"
|
||||
checksum = "6938541d1948a543bca23303fec4cff9c36bf0e63b8fa3ae1b337bcb9d5b81af"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1295,9 +1297,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-kinesis"
|
||||
version = "1.62.0"
|
||||
version = "1.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31622345afd0c35d33c1cbba73ccf9fb88e09857413d8963dea2c493e00704d"
|
||||
checksum = "89f2163d8704e8fdcd51ec6c2e0441c418471e422ee9690451b17a1c46344e1a"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1317,9 +1319,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.77.0"
|
||||
version = "1.76.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34e87342432a3de0e94e82c99a7cbd9042f99de029ae1f4e368160f9e9929264"
|
||||
checksum = "66e83401ad7287ad15244d557e35502c2a94105ca5b41d656c391f1a4fc04ca2"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1351,9 +1353,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.60.0"
|
||||
version = "1.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60186fab60b24376d3e33b9ff0a43485f99efd470e3b75a9160c849741d63d56"
|
||||
checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1373,9 +1375,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.61.0"
|
||||
version = "1.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7033130ce1ee13e6018905b7b976c915963755aef299c1521897679d6cd4f8ef"
|
||||
checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1395,9 +1397,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.61.0"
|
||||
version = "1.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5c1cac7677179d622b4448b0d31bcb359185295dc6fca891920cfb17e2b5156"
|
||||
checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1458,9 +1460,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-checksums"
|
||||
version = "0.63.0"
|
||||
version = "0.62.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2dc8d842d872529355c72632de49ef8c5a2949a4472f10e802f28cf925770c"
|
||||
checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
@@ -1810,7 +1812,7 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
@@ -1833,7 +1835,7 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
@@ -2404,6 +2406,25 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbindgen"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"heck 0.4.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn 2.0.90",
|
||||
"tempfile",
|
||||
"toml 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbindgen"
|
||||
version = "0.28.0"
|
||||
@@ -2501,9 +2522,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.40"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
@@ -2511,7 +2532,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3508,10 +3529,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crc64fast-nvme"
|
||||
version = "1.2.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4955638f00a809894c947f85a024020a20815b65a5eea633798ea7924edab2b3"
|
||||
checksum = "d5e2ee08013e3f228d6d2394116c4549a6df77708442c62d887d83f68ef2ee37"
|
||||
dependencies = [
|
||||
"cbindgen 0.27.0",
|
||||
"crc",
|
||||
]
|
||||
|
||||
@@ -4290,6 +4312,12 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
@@ -5353,6 +5381,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
"text",
|
||||
"time",
|
||||
"unindent",
|
||||
@@ -5399,8 +5428,10 @@ dependencies = [
|
||||
"buffer_diff",
|
||||
"collections",
|
||||
"component",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -5408,7 +5439,9 @@ dependencies = [
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"linkify",
|
||||
"linkme",
|
||||
"log",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"panel",
|
||||
@@ -5560,7 +5593,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cbindgen",
|
||||
"cbindgen 0.28.0",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -7233,9 +7266,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.170"
|
||||
version = "0.2.169"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -7640,6 +7673,25 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lua-src"
|
||||
version = "547.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "luajit-src"
|
||||
version = "210.5.12+a4f56a4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"which 7.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lyon"
|
||||
version = "1.0.1"
|
||||
@@ -8053,6 +8105,33 @@ dependencies = [
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3f763c1041eff92ffb5d7169968a327e1ed2ebfe425dac0ee5a35f29082534b"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"either",
|
||||
"mlua-sys",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua-sys"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"lua-src",
|
||||
"luajit-src",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msvc_spectre_libs"
|
||||
version = "0.1.2"
|
||||
@@ -9762,15 +9841,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pgvector"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0e8871b6d7ca78348c6cd29b911b94851f3429f0cd403130ca17f26c1fb91a6"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
@@ -10410,7 +10480,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
||||
dependencies = [
|
||||
"bytes 1.10.0",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"multimap 0.10.0",
|
||||
"once_cell",
|
||||
@@ -10443,7 +10513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
@@ -11471,9 +11541,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.6.0"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f"
|
||||
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
@@ -11482,9 +11552,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.6.0"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae"
|
||||
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11495,9 +11565,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.6.0"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a"
|
||||
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||
dependencies = [
|
||||
"globset",
|
||||
"sha2",
|
||||
@@ -11772,9 +11842,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
@@ -11785,9 +11855,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11813,6 +11883,28 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
|
||||
|
||||
[[package]]
|
||||
name = "scripting_tool"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"collections",
|
||||
"gpui",
|
||||
"language",
|
||||
"mlua",
|
||||
"parking_lot",
|
||||
"regex",
|
||||
"rich_text",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.11.0"
|
||||
@@ -11850,18 +11942,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm"
|
||||
version = "1.1.6"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13fba7b2c749b2d0a00303d5cb13e6761e39a4172554bdf930852cac4e7aeabd"
|
||||
checksum = "00733e5418e8ae3758cdb988c3654174e716230cc53ee2cb884207cf86a23029"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"bigdecimal",
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"futures 0.3.31",
|
||||
"log",
|
||||
"ouroboros",
|
||||
"pgvector",
|
||||
"rust_decimal",
|
||||
"sea-orm-macros",
|
||||
"sea-query",
|
||||
@@ -11879,9 +11970,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm-macros"
|
||||
version = "1.1.6"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2568cff8d35d5150b4276cc0dd766192a587f64b6ece60ae3706e0872c4eb209"
|
||||
checksum = "a98408f82fb4875d41ef469a79944a7da29767c7b3e4028e22188a3dd613b10f"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
@@ -14801,9 +14892,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.15.1"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
|
||||
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"serde",
|
||||
@@ -14905,6 +14996,8 @@ dependencies = [
|
||||
"multi_buffer",
|
||||
"nvim-rs",
|
||||
"parking_lot",
|
||||
"picker",
|
||||
"project",
|
||||
"project_panel",
|
||||
"regex",
|
||||
"release_channel",
|
||||
@@ -15707,6 +15800,18 @@ dependencies = [
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "7.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283"
|
||||
dependencies = [
|
||||
"either",
|
||||
"env_home",
|
||||
"rustix",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.5.2"
|
||||
@@ -15926,12 +16031,6 @@ dependencies = [
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
@@ -16767,7 +16866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.177.0"
|
||||
version = "0.178.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -16853,6 +16952,7 @@ dependencies = [
|
||||
"repl",
|
||||
"reqwest_client",
|
||||
"rope",
|
||||
"scripting_tool",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -117,6 +117,7 @@ members = [
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/schema_generator",
|
||||
"crates/scripting_tool",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
@@ -321,6 +322,7 @@ reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
scripting_tool = { path = "crates/scripting_tool" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
@@ -453,6 +455,7 @@ livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "
|
||||
], default-features = false }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { version = "0.10.0" }
|
||||
nix = "0.29"
|
||||
|
||||
1
assets/icons/cloud.svg
Normal file
1
assets/icons/cloud.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></svg>
|
||||
|
After Width: | Height: | Size: 279 B |
@@ -1,12 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2131_1193)">
|
||||
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="1" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2131_1193">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M7 11H8M8 11H9M8 11V8.1C8 8.04477 7.95523 8 7.9 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M8 6.5C8.55228 6.5 9 6.05228 9 5.5C9 4.94772 8.55228 4.5 8 4.5C7.44772 4.5 7 4.94772 7 5.5C7 6.05228 7.44772 6.5 8 6.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 479 B After Width: | Height: | Size: 524 B |
4
assets/icons/zed_predict_error.svg
Normal file
4
assets/icons/zed_predict_error.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.6" d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M14 8L10 12M14 12L10 8" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 296 B |
@@ -10,8 +10,8 @@
|
||||
"pagedown": "menu::SelectLast",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"tab": "menu::SelectNext",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
"ctrl-p": "menu::SelectPrevious",
|
||||
"shift-tab": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
@@ -38,14 +38,14 @@
|
||||
{
|
||||
"context": "Picker || menu",
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Prompt",
|
||||
"bindings": {
|
||||
"left": "menu::SelectPrev",
|
||||
"left": "menu::SelectPrevious",
|
||||
"right": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
@@ -57,7 +57,7 @@
|
||||
"backspace": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"shift-tab": "editor::Backtab",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
// "ctrl-t": "editor::Transpose",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
@@ -180,7 +180,7 @@
|
||||
"ctrl-k c": "assistant::CopyCode",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPrevMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-k h": "assistant::DeployHistory",
|
||||
"ctrl-k l": "assistant::DeployPromptLibrary",
|
||||
@@ -203,7 +203,7 @@
|
||||
"escape": "buffer_search::Dismiss",
|
||||
"tab": "buffer_search::FocusEditor",
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"shift-enter": "search::SelectPreviousMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"find": "search::FocusSearch",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
@@ -272,7 +272,7 @@
|
||||
"alt-8": ["pane::ActivateItem", 7],
|
||||
"alt-9": ["pane::ActivateItem", 8],
|
||||
"alt-0": "pane::ActivateLastItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-shift-pageup": "pane::SwapItemLeft",
|
||||
"ctrl-shift-pagedown": "pane::SwapItemRight",
|
||||
@@ -290,8 +290,8 @@
|
||||
"forward": "pane::GoForward",
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPrevMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPreviousMatch",
|
||||
"shift-f3": "search::SelectPreviousMatch",
|
||||
"shift-find": "project_search::ToggleFocus",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
"ctrl-alt-shift-h": "search::ToggleReplace",
|
||||
@@ -334,7 +334,7 @@
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPrevDiagnostic",
|
||||
"shift-f8": "editor::GoToPreviousDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
@@ -370,10 +370,10 @@
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": ["git::StageAndNext", { "whole_excerpt": false }],
|
||||
"alt-shift-y": ["git::UnstageAndNext", { "whole_excerpt": false }],
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext",
|
||||
"alt-.": "editor::GoToHunk",
|
||||
"alt-,": "editor::GoToPrevHunk"
|
||||
"alt-,": "editor::GoToPreviousHunk"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -536,8 +536,8 @@
|
||||
{
|
||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::ContextMenuPrev",
|
||||
"up": "editor::ContextMenuPrev",
|
||||
"ctrl-p": "editor::ContextMenuPrevious",
|
||||
"up": "editor::ContextMenuPrevious",
|
||||
"ctrl-n": "editor::ContextMenuNext",
|
||||
"down": "editor::ContextMenuNext",
|
||||
"pageup": "editor::ContextMenuFirst",
|
||||
@@ -565,7 +565,7 @@
|
||||
"ctrl-alt-enter": "editor::OpenExcerptsSplit",
|
||||
"ctrl-shift-e": "pane::RevealInProjectPanel",
|
||||
"ctrl-f8": "editor::GoToHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPrevHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPreviousHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
@@ -612,12 +612,29 @@
|
||||
"ctrl-alt-e": "assistant2::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2 && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewPromptEditor",
|
||||
"cmd-alt-t": "assistant2::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "assistant2::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
@@ -662,7 +679,7 @@
|
||||
"alt-ctrl-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"ctrl-alt-enter": "editor::OpenExcerptsSplit"
|
||||
}
|
||||
@@ -700,7 +717,7 @@
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
@@ -713,7 +730,7 @@
|
||||
{
|
||||
"context": "GitPanel && ChangesList",
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"space": "git::ToggleStaged",
|
||||
@@ -722,7 +739,7 @@
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-enter": "git::ShowCommitEditor",
|
||||
"alt-enter": "menu::SecondaryConfirm"
|
||||
}
|
||||
},
|
||||
@@ -736,7 +753,7 @@
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "git::Commit"
|
||||
"ctrl-enter": "git::ShowCommitEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -749,14 +766,6 @@
|
||||
"alt-up": "git_panel::FocusChanges"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
@@ -779,6 +788,9 @@
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"tab": "picker::ConfirmCompletion",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }]
|
||||
}
|
||||
@@ -792,7 +804,7 @@
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
|
||||
"bindings": {
|
||||
"ctrl-shift-p": "file_finder::SelectPrev",
|
||||
"ctrl-shift-p": "file_finder::SelectPrevious",
|
||||
"ctrl-j": "pane::SplitDown",
|
||||
"ctrl-k": "pane::SplitUp",
|
||||
"ctrl-h": "pane::SplitLeft",
|
||||
@@ -802,8 +814,8 @@
|
||||
{
|
||||
"context": "TabSwitcher",
|
||||
"bindings": {
|
||||
"ctrl-shift-tab": "menu::SelectPrev",
|
||||
"ctrl-up": "menu::SelectPrev",
|
||||
"ctrl-shift-tab": "menu::SelectPrevious",
|
||||
"ctrl-up": "menu::SelectPrevious",
|
||||
"ctrl-down": "menu::SelectNext",
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
[
|
||||
// Moved before Standard macOS bindings so that `cmd-w` is not the last binding for
|
||||
// `workspace::CloseWindow` and displayed/intercepted by macOS
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "prompt_library::NewPrompt",
|
||||
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
// Standard macOS bindings
|
||||
{
|
||||
"use_key_equivalents": true,
|
||||
@@ -14,19 +25,19 @@
|
||||
"tab": "menu::SelectNext",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"down": "menu::SelectNext",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"up": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrevious",
|
||||
"ctrl-p": "menu::SelectPrevious",
|
||||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
@@ -54,7 +65,7 @@
|
||||
"ctrl-d": "editor::Delete",
|
||||
"delete": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"shift-tab": "editor::Backtab",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
"ctrl-k": "editor::KillRingCut",
|
||||
"ctrl-y": "editor::KillRingYank",
|
||||
@@ -131,8 +142,8 @@
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-alt-z": "git::Restore",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"cmd-y": ["git::StageAndNext", { "whole_excerpt": false }],
|
||||
"cmd-shift-y": ["git::UnstageAndNext", { "whole_excerpt": false }],
|
||||
"cmd-y": "git::StageAndNext",
|
||||
"cmd-shift-y": "git::UnstageAndNext",
|
||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
@@ -208,7 +219,7 @@
|
||||
"cmd-k c": "assistant::CopyCode",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-k h": "assistant::DeployHistory",
|
||||
"cmd-k l": "assistant::DeployPromptLibrary",
|
||||
@@ -245,6 +256,14 @@
|
||||
"cmd-alt-e": "assistant2::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2 && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewPromptEditor",
|
||||
"cmd-alt-t": "assistant2::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -252,6 +271,15 @@
|
||||
"enter": "assistant2::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"use_key_equivalents": true,
|
||||
@@ -270,15 +298,6 @@
|
||||
"backspace": "assistant2::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "prompt_library::NewPrompt",
|
||||
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"use_key_equivalents": true,
|
||||
@@ -286,7 +305,7 @@
|
||||
"escape": "buffer_search::Dismiss",
|
||||
"tab": "buffer_search::FocusEditor",
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"shift-enter": "search::SelectPreviousMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "search::ToggleReplace",
|
||||
@@ -353,8 +372,8 @@
|
||||
"context": "Pane",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"alt-cmd-left": "pane::ActivatePreviousItem",
|
||||
"cmd-{": "pane::ActivatePreviousItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"ctrl-shift-pageup": "pane::SwapItemLeft",
|
||||
@@ -368,7 +387,7 @@
|
||||
"cmd-k cmd-w": ["pane::CloseAllItems", { "close_pinned": false }],
|
||||
"cmd-f": "project_search::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
@@ -408,7 +427,7 @@
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPrevDiagnostic",
|
||||
"shift-f8": "editor::GoToPreviousDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
@@ -614,8 +633,8 @@
|
||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "editor::ContextMenuPrev",
|
||||
"ctrl-p": "editor::ContextMenuPrev",
|
||||
"up": "editor::ContextMenuPrevious",
|
||||
"ctrl-p": "editor::ContextMenuPrevious",
|
||||
"down": "editor::ContextMenuNext",
|
||||
"ctrl-n": "editor::ContextMenuNext",
|
||||
"pageup": "editor::ContextMenuFirst",
|
||||
@@ -641,7 +660,7 @@
|
||||
"cmd-alt-enter": "editor::OpenExcerptsSplit",
|
||||
"cmd-shift-e": "pane::RevealInProjectPanel",
|
||||
"cmd-f8": "editor::GoToHunk",
|
||||
"cmd-shift-f8": "editor::GoToPrevHunk",
|
||||
"cmd-shift-f8": "editor::GoToPreviousHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
@@ -684,7 +703,7 @@
|
||||
"alt-cmd-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"cmd-alt-enter": "editor::OpenExcerptsSplit"
|
||||
}
|
||||
@@ -714,7 +733,7 @@
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
@@ -729,7 +748,7 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
@@ -741,14 +760,14 @@
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::Commit"
|
||||
"cmd-enter": "git::ShowCommitEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "git::Commit"
|
||||
"cmd-enter": "git::ShowCommitEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -796,6 +815,9 @@
|
||||
"context": "Picker > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"tab": "picker::ConfirmCompletion",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
|
||||
@@ -812,7 +834,7 @@
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-p": "file_finder::SelectPrev",
|
||||
"cmd-shift-p": "file_finder::SelectPrevious",
|
||||
"cmd-j": "pane::SplitDown",
|
||||
"cmd-k": "pane::SplitUp",
|
||||
"cmd-h": "pane::SplitLeft",
|
||||
@@ -823,8 +845,8 @@
|
||||
"context": "TabSwitcher",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-tab": "menu::SelectPrev",
|
||||
"ctrl-up": "menu::SelectPrev",
|
||||
"ctrl-shift-tab": "menu::SelectPrevious",
|
||||
"ctrl-up": "menu::SelectPrevious",
|
||||
"ctrl-down": "menu::SelectNext",
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
|
||||
"ctrl-shift-f3": "search::SelectPrevMatch" // find-and-replace:find-previous-selected
|
||||
"ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPrevMatch",
|
||||
"ctrl-r": "search::SelectPreviousMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-s": "zed::OpenSettings",
|
||||
"ctrl-{": "pane::ActivatePrevItem",
|
||||
"ctrl-{": "pane::ActivatePreviousItem",
|
||||
"ctrl-}": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
@@ -41,9 +41,9 @@
|
||||
"ctrl-shift-b": "editor::GoToTypeDefinition",
|
||||
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"shift-f2": "editor::GoToPrevDiagnostic",
|
||||
"shift-f2": "editor::GoToPreviousDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPreviousHunk",
|
||||
"ctrl-alt-z": "git::Restore",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-{": "pane::ActivatePrevItem",
|
||||
"ctrl-{": "pane::ActivatePreviousItem",
|
||||
"ctrl-}": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
@@ -44,7 +44,7 @@
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
@@ -62,7 +62,7 @@
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"f4": "search::SelectNextMatch",
|
||||
"shift-f4": "search::SelectPrevMatch",
|
||||
"shift-f4": "search::SelectPreviousMatch",
|
||||
"alt-1": ["pane::ActivateItem", 0],
|
||||
"alt-2": ["pane::ActivateItem", 1],
|
||||
"alt-3": ["pane::ActivateItem", 2],
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"cmd-f3": "search::SelectNextMatch",
|
||||
"cmd-shift-f3": "search::SelectPrevMatch"
|
||||
"cmd-shift-f3": "search::SelectPreviousMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPrevMatch",
|
||||
"ctrl-r": "search::SelectPreviousMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-{": "pane::ActivatePreviousItem",
|
||||
"cmd-}": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
@@ -39,9 +39,9 @@
|
||||
"cmd-shift-b": "editor::GoToTypeDefinition",
|
||||
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"shift-f2": "editor::GoToPrevDiagnostic",
|
||||
"shift-f2": "editor::GoToPreviousDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPreviousHunk",
|
||||
"cmd-home": "editor::MoveToBeginning",
|
||||
"cmd-end": "editor::MoveToEnd",
|
||||
"cmd-shift-home": "editor::SelectToBeginning",
|
||||
@@ -61,7 +61,7 @@
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"shift-enter": "search::SelectPrevMatch"
|
||||
"shift-enter": "search::SelectPreviousMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-{": "pane::ActivatePreviousItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
@@ -45,7 +45,7 @@
|
||||
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
|
||||
"alt-shift-cmd-down": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
@@ -64,7 +64,7 @@
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"f4": "search::SelectNextMatch",
|
||||
"shift-f4": "search::SelectPrevMatch",
|
||||
"shift-f4": "search::SelectPreviousMatch",
|
||||
"cmd-1": ["pane::ActivateItem", 0],
|
||||
"cmd-2": ["pane::ActivateItem", 1],
|
||||
"cmd-3": ["pane::ActivateItem", 2],
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-shift-s": "search::SelectPrevMatch"
|
||||
"ctrl-shift-s": "search::SelectPreviousMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"tab": "menu::SelectNext",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"down": "menu::SelectNext",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"up": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrevious",
|
||||
"ctrl-p": "menu::SelectPrevious",
|
||||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
|
||||
@@ -62,9 +62,9 @@
|
||||
"g /": "pane::DeploySearch",
|
||||
"?": ["vim::Search", { "backwards": true }],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"#": "vim::MoveToPrevious",
|
||||
"n": "vim::MoveToNextMatch",
|
||||
"shift-n": "vim::MoveToPrevMatch",
|
||||
"shift-n": "vim::MoveToPreviousMatch",
|
||||
"%": "vim::Matching",
|
||||
"] }": ["vim::UnmatchedForward", { "char": "}" }],
|
||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
|
||||
@@ -106,7 +106,7 @@
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
"g shift-t": "pane::ActivatePrevItem",
|
||||
"g shift-t": "pane::ActivatePreviousItem",
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToDeclaration",
|
||||
"g y": "editor::GoToTypeDefinition",
|
||||
@@ -126,7 +126,7 @@
|
||||
"g shift-a": "editor::FindAllReferences", // zed specific
|
||||
"g space": "editor::OpenExcerpts", // zed specific
|
||||
"g *": ["vim::MoveToNext", { "partial_word": true }],
|
||||
"g #": ["vim::MoveToPrev", { "partial_word": true }],
|
||||
"g #": ["vim::MoveToPrevious", { "partial_word": true }],
|
||||
"g j": ["vim::Down", { "display_lines": true }],
|
||||
"g down": ["vim::Down", { "display_lines": true }],
|
||||
"g k": ["vim::Up", { "display_lines": true }],
|
||||
@@ -138,7 +138,7 @@
|
||||
"g ^": ["vim::FirstNonWhitespace", { "display_lines": true }],
|
||||
"g v": "vim::RestoreVisualSelection",
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
"g [": "editor::GoToPreviousDiagnostic",
|
||||
"g i": "vim::InsertAtPrevious",
|
||||
"g ,": "vim::ChangeListNewer",
|
||||
"g ;": "vim::ChangeListOlder",
|
||||
@@ -231,15 +231,15 @@
|
||||
"g w": "vim::PushRewrap",
|
||||
"g q": "vim::PushRewrap",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"insert": "vim::InsertBefore",
|
||||
// tree-sitter related commands
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
"] x": "vim::SelectSmallerSyntaxNode",
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPrevDiagnostic",
|
||||
"[ d": "editor::GoToPreviousDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
"[ c": "editor::GoToPrevHunk",
|
||||
"[ c": "editor::GoToPreviousHunk",
|
||||
"g c": "vim::PushToggleComments"
|
||||
}
|
||||
},
|
||||
@@ -272,7 +272,7 @@
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"~": "vim::ChangeCase",
|
||||
"*": ["vim::MoveToNext", { "partial_word": true }],
|
||||
"#": ["vim::MoveToPrev", { "partial_word": true }],
|
||||
"#": ["vim::MoveToPrevious", { "partial_word": true }],
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"g ctrl-a": ["vim::Increment", { "step": true }],
|
||||
@@ -448,7 +448,10 @@
|
||||
"d": "vim::CurrentLine",
|
||||
"s": "vim::PushDeleteSurrounds",
|
||||
"o": "editor::ToggleSelectedDiffHunks", // "d o"
|
||||
"p": "git::Restore" // "d p"
|
||||
"shift-o": "git::ToggleStaged",
|
||||
"p": "git::Restore", // "d p"
|
||||
"u": "git::StageAndNext", // "d u"
|
||||
"shift-u": "git::UnstageAndNext" // "d shift-u"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -620,8 +623,8 @@
|
||||
"ctrl-w =": "vim::ResetPaneSizes",
|
||||
"ctrl-w g t": "pane::ActivateNextItem",
|
||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||
"ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePreviousItem",
|
||||
"ctrl-w ctrl-g shift-t": "pane::ActivatePreviousItem",
|
||||
"ctrl-w w": "workspace::ActivateNextPane",
|
||||
"ctrl-w ctrl-w": "workspace::ActivateNextPane",
|
||||
"ctrl-w p": "workspace::ActivatePreviousPane",
|
||||
@@ -664,7 +667,7 @@
|
||||
"escape": "project_panel::ToggleFocus",
|
||||
"h": "project_panel::CollapseSelectedEntry",
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrev",
|
||||
"k": "menu::SelectPrevious",
|
||||
"l": "project_panel::ExpandSelectedEntry",
|
||||
"o": "project_panel::OpenPermanent",
|
||||
"shift-d": "project_panel::Delete",
|
||||
@@ -690,7 +693,7 @@
|
||||
"context": "OutlinePanel && not_editing",
|
||||
"bindings": {
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrev",
|
||||
"k": "menu::SelectPrevious",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst"
|
||||
}
|
||||
@@ -699,7 +702,7 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"k": "menu::SelectPrev",
|
||||
"k": "menu::SelectPrevious",
|
||||
"j": "menu::SelectNext",
|
||||
"g g": "menu::SelectFirst",
|
||||
"shift-g": "menu::SelectLast",
|
||||
|
||||
@@ -648,11 +648,19 @@
|
||||
// Show git status colors in the editor tabs.
|
||||
"git_status": false,
|
||||
// Position of the close button on the editor tabs.
|
||||
// One of: ["right", "left", "hidden"]
|
||||
"close_position": "right",
|
||||
// Whether to show the file icon for a tab.
|
||||
"file_icons": false,
|
||||
// Whether to always show the close button on tabs.
|
||||
"always_show_close_button": false,
|
||||
// Controls the appearance behavior of the tab's close button.
|
||||
//
|
||||
// 1. Show it just upon hovering the tab. (default)
|
||||
// "hover"
|
||||
// 2. Show it persistently.
|
||||
// "always"
|
||||
// 3. Never show it, even if hovering it.
|
||||
// "hidden"
|
||||
"show_close_button": "hover",
|
||||
// What to do after closing the current tab.
|
||||
//
|
||||
// 1. Activate the tab that was open previously (default)
|
||||
@@ -829,7 +837,15 @@
|
||||
//
|
||||
// The minimum column number to show the inline blame information at
|
||||
// "min_column": 0
|
||||
}
|
||||
},
|
||||
// How git hunks are displayed visually in the editor.
|
||||
// This setting can take two values:
|
||||
//
|
||||
// 1. Show unstaged hunks with a transparent background (default):
|
||||
// "hunk_style": "transparent"
|
||||
// 2. Show unstaged hunks with a pattern background:
|
||||
// "hunk_style": "pattern"
|
||||
"hunk_style": "transparent"
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration using `direnv export json` directly.
|
||||
@@ -843,15 +859,7 @@
|
||||
// Any addition to this list will be merged with the default list.
|
||||
// Globs are matched relative to the worktree root,
|
||||
// except when starting with a slash (/) or equivalent in Windows.
|
||||
"disabled_globs": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/.dev.vars",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
|
||||
// When to show edit predictions previews in buffer.
|
||||
// This setting takes two possible values:
|
||||
// 1. Display predictions inline when there are no language server completions available.
|
||||
|
||||
@@ -35,7 +35,7 @@ use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
};
|
||||
use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector};
|
||||
use language_model_selector::inline_language_model_selector;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
@@ -1425,7 +1425,6 @@ enum PromptEditorEvent {
|
||||
struct PromptEditor {
|
||||
id: InlineAssistId,
|
||||
editor: Entity<Editor>,
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
@@ -1439,6 +1438,7 @@ struct PromptEditor {
|
||||
_token_count_subscriptions: Vec<Subscription>,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
show_rate_limit_notice: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -1567,6 +1567,7 @@ impl Render for PromptEditor {
|
||||
]
|
||||
}
|
||||
});
|
||||
let fs_clone = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.key_context("PromptEditor")
|
||||
@@ -1589,10 +1590,13 @@ impl Render for PromptEditor {
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
InlineLanguageModelSelector::new(self.language_model_selector.clone())
|
||||
.render(window, cx),
|
||||
)
|
||||
.child(inline_language_model_selector(move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}))
|
||||
.map(|el| {
|
||||
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
|
||||
return el;
|
||||
@@ -1705,21 +1709,8 @@ impl PromptEditor {
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
fs,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
gutter_dimensions,
|
||||
prompt_history,
|
||||
|
||||
@@ -19,7 +19,7 @@ use language_model::{
|
||||
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector};
|
||||
use language_model_selector::inline_language_model_selector;
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use std::{
|
||||
@@ -487,9 +487,9 @@ enum PromptEditorEvent {
|
||||
|
||||
struct PromptEditor {
|
||||
id: TerminalInlineAssistId,
|
||||
fs: Arc<dyn Fs>,
|
||||
height_in_lines: u8,
|
||||
editor: Entity<Editor>,
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
@@ -506,7 +506,7 @@ struct PromptEditor {
|
||||
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let status = &self.codegen.read(cx).status;
|
||||
let buttons = match status {
|
||||
CodegenStatus::Idle => {
|
||||
@@ -624,6 +624,8 @@ impl Render for PromptEditor {
|
||||
}
|
||||
};
|
||||
|
||||
let fs_clone = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_y_1()
|
||||
@@ -641,10 +643,13 @@ impl Render for PromptEditor {
|
||||
.w_12()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
InlineLanguageModelSelector::new(self.language_model_selector.clone())
|
||||
.render(window, cx),
|
||||
)
|
||||
.child(inline_language_model_selector(move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}))
|
||||
.children(
|
||||
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
@@ -722,22 +727,9 @@ impl PromptEditor {
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
fs,
|
||||
height_in_lines: 1,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use assistant_tool::{ToolRegistry, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
list, AbsoluteLength, AnyElement, App, DefiniteLength, EdgesRefinement, Empty, Entity, Length,
|
||||
ListAlignment, ListOffset, ListState, StyleRefinement, Subscription, TextStyleRefinement,
|
||||
UnderlineStyle, WeakEntity,
|
||||
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
||||
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language::{Buffer, Language, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use settings::Settings as _;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Disclosure};
|
||||
use ui::{prelude::*, Disclosure, KeyBinding};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
|
||||
@@ -26,14 +27,21 @@ pub struct ActiveThread {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
thread: Entity<Thread>,
|
||||
save_thread_task: Option<Task<()>>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||
last_error: Option<ThreadError>,
|
||||
lua_language: Option<Arc<Language>>, // Used for syntax highlighting in the Lua script tool
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct EditMessageState {
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl ActiveThread {
|
||||
pub fn new(
|
||||
thread: Entity<Thread>,
|
||||
@@ -55,20 +63,36 @@ impl ActiveThread {
|
||||
tools,
|
||||
thread_store,
|
||||
thread: thread.clone(),
|
||||
save_thread_task: None,
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, _: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, cx))
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
}),
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
lua_language: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
// Initialize the Lua language in the background, for syntax highlighting.
|
||||
let language_registry = this.language_registry.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Ok(lua_language) = language_registry.language_for_name("Lua").await {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.lua_language = Some(lua_language);
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
this.push_message(&message.id, message.text.clone(), window, cx);
|
||||
}
|
||||
@@ -117,6 +141,44 @@ impl ActiveThread {
|
||||
self.messages.push(*id);
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
|
||||
let markdown = self.render_markdown(text.into(), window, cx);
|
||||
self.rendered_messages_by_id.insert(*id, markdown);
|
||||
self.list_state.scroll_to(ListOffset {
|
||||
item_ix: old_len,
|
||||
offset_in_item: Pixels(0.0),
|
||||
});
|
||||
}
|
||||
|
||||
fn edited_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
text: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
|
||||
return;
|
||||
};
|
||||
self.list_state.splice(index..index + 1, 1);
|
||||
let markdown = self.render_markdown(text.into(), window, cx);
|
||||
self.rendered_messages_by_id.insert(*id, markdown);
|
||||
}
|
||||
|
||||
fn deleted_message(&mut self, id: &MessageId) {
|
||||
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
|
||||
return;
|
||||
};
|
||||
self.messages.remove(index);
|
||||
self.list_state.splice(index..index + 1, 0);
|
||||
self.rendered_messages_by_id.remove(id);
|
||||
}
|
||||
|
||||
fn render_markdown(
|
||||
&self,
|
||||
text: SharedString,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Markdown> {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
@@ -134,6 +196,8 @@ impl ActiveThread {
|
||||
base_text_style: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
code_block_overflow_x_scroll: true,
|
||||
table_overflow_x_scroll: true,
|
||||
code_block: StyleRefinement {
|
||||
margin: EdgesRefinement {
|
||||
top: Some(Length::Definite(rems(0.).into())),
|
||||
@@ -180,20 +244,15 @@ impl ActiveThread {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let markdown = cx.new(|cx| {
|
||||
cx.new(|cx| {
|
||||
Markdown::new(
|
||||
text.into(),
|
||||
text,
|
||||
markdown_style,
|
||||
Some(self.language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.rendered_messages_by_id.insert(*id, markdown);
|
||||
self.list_state.scroll_to(ListOffset {
|
||||
item_ix: old_len,
|
||||
offset_in_item: Pixels(0.0),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
@@ -208,11 +267,7 @@ impl ActiveThread {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
|
||||
self.thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store.save_thread(&self.thread, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.save_thread(cx);
|
||||
}
|
||||
ThreadEvent::StreamedAssistantText(message_id, text) => {
|
||||
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
|
||||
@@ -231,18 +286,31 @@ impl ActiveThread {
|
||||
self.push_message(message_id, message_text, window, cx);
|
||||
}
|
||||
|
||||
self.thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store.save_thread(&self.thread, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::MessageEdited(message_id) => {
|
||||
if let Some(message_text) = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(*message_id)
|
||||
.map(|message| message.text.clone())
|
||||
{
|
||||
self.edited_message(message_id, message_text, window, cx);
|
||||
}
|
||||
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::MessageDeleted(message_id) => {
|
||||
self.deleted_message(message_id);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
.thread
|
||||
.read(cx)
|
||||
let thread = self.thread.read(cx);
|
||||
let thread_id = thread.id().0.clone();
|
||||
let pending_tool_uses = thread
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
@@ -251,7 +319,13 @@ impl ActiveThread {
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), window, cx);
|
||||
let task = tool.run(
|
||||
tool_use.input,
|
||||
thread_id.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_tool_output(tool_use.id.clone(), task, cx);
|
||||
@@ -287,7 +361,133 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
/// Spawns a task to save the active thread.
|
||||
///
|
||||
/// Only one task to save the thread will be in flight at a time.
|
||||
fn save_thread(&mut self, cx: &mut Context<Self>) {
|
||||
let thread = self.thread.clone();
|
||||
self.save_thread_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
let task = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.thread_store
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
|
||||
})
|
||||
.ok();
|
||||
|
||||
if let Some(task) = task {
|
||||
task.await.log_err();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn start_editing_message(
|
||||
&mut self,
|
||||
message_id: MessageId,
|
||||
message_text: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let buffer = cx.new(|cx| {
|
||||
MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
|
||||
});
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
editor::EditorMode::AutoHeight { max_lines: 8 },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
editor
|
||||
});
|
||||
self.editing_message = Some((
|
||||
message_id,
|
||||
EditMessageState {
|
||||
editor: editor.clone(),
|
||||
},
|
||||
));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editing_message.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm_editing_message(
|
||||
&mut self,
|
||||
_: &menu::Confirm,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some((message_id, state)) = self.editing_message.take() else {
|
||||
return;
|
||||
};
|
||||
let edited_text = state.editor.read(cx).text(cx);
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.edit_message(message_id, Role::User, edited_text, cx);
|
||||
for message_id in self.messages_after(message_id) {
|
||||
thread.delete_message(*message_id, cx);
|
||||
}
|
||||
});
|
||||
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
if provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||
{
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.active_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.send_to_model(model, RequestKind::Chat, false, cx)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
|
||||
self.messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|message_id| {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.message(**message_id)
|
||||
.map_or(false, |message| message.role == Role::User)
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
|
||||
self.messages
|
||||
.iter()
|
||||
.position(|id| *id == message_id)
|
||||
.map(|index| &self.messages[index + 1..])
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_editing_message(&menu::Cancel, window, cx);
|
||||
}
|
||||
|
||||
fn handle_regenerate_click(
|
||||
&mut self,
|
||||
_: &ClickEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.confirm_editing_message(&menu::Confirm, window, cx);
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let message_id = self.messages[ix];
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
return Empty.into_any();
|
||||
@@ -306,8 +506,28 @@ impl ActiveThread {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
let allow_editing_message =
|
||||
message.role == Role::User && self.last_user_message(cx) == Some(message_id);
|
||||
|
||||
let edit_message_editor = self
|
||||
.editing_message
|
||||
.as_ref()
|
||||
.filter(|(id, _)| *id == message_id)
|
||||
.map(|(_, state)| state.editor.clone());
|
||||
|
||||
let message_content = v_flex()
|
||||
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
|
||||
.child(
|
||||
if let Some(edit_message_editor) = edit_message_editor.clone() {
|
||||
div()
|
||||
.key_context("EditMessageEditor")
|
||||
.on_action(cx.listener(Self::cancel_editing_message))
|
||||
.on_action(cx.listener(Self::confirm_editing_message))
|
||||
.p_2p5()
|
||||
.child(edit_message_editor)
|
||||
} else {
|
||||
div().p_2p5().text_ui(cx).child(markdown.clone())
|
||||
},
|
||||
)
|
||||
.when_some(context, |parent, context| {
|
||||
if !context.is_empty() {
|
||||
parent.child(
|
||||
@@ -337,7 +557,8 @@ impl ActiveThread {
|
||||
.child(
|
||||
h_flex()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.bg(colors.editor_foreground.opacity(0.05))
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
@@ -356,6 +577,71 @@ impl ActiveThread {
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.when_some(
|
||||
edit_message_editor.clone(),
|
||||
|this, edit_message_editor| {
|
||||
let focus_handle = edit_message_editor.focus_handle(cx);
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel-edit-message", "Cancel")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(Self::handle_cancel_click),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
"confirm-edit-message",
|
||||
"Regenerate",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(Self::handle_regenerate_click),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(
|
||||
edit_message_editor.is_none() && allow_editing_message,
|
||||
|this| {
|
||||
this.child(
|
||||
Button::new("edit-message", "Edit")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let message_text = message.text.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.start_editing_message(
|
||||
message_id,
|
||||
message_text.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(message_content),
|
||||
@@ -388,6 +674,7 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let tool = ToolRegistry::global(cx).tool(&tool_use.name);
|
||||
let is_open = self
|
||||
.expanded_tool_uses
|
||||
.get(&tool_use.id)
|
||||
@@ -453,11 +740,17 @@ impl ActiveThread {
|
||||
.px_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Label::new("Input:"))
|
||||
.child(Label::new(
|
||||
serde_json::to_string_pretty(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
)),
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(match tool.clone() {
|
||||
Some(tool) => tool.render_input(
|
||||
tool_use.input,
|
||||
self.lua_language.clone(),
|
||||
cx,
|
||||
),
|
||||
None => {
|
||||
assistant_tool::default_render_input(tool_use.input)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(|parent| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => parent.child(
|
||||
@@ -465,16 +758,17 @@ impl ActiveThread {
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Result:"))
|
||||
.child(Label::new(output)),
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(match tool {
|
||||
Some(tool) => tool.render_output(output, cx),
|
||||
None => assistant_tool::default_render_output(output),
|
||||
}),
|
||||
),
|
||||
ToolUseStatus::Error(err) => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Error:"))
|
||||
.child(Label::new(err)),
|
||||
v_flex().gap_0p5().py_1().px_2p5().child(match tool {
|
||||
Some(tool) => tool.render_error(err, cx),
|
||||
None => assistant_tool::default_render_output(err),
|
||||
}),
|
||||
),
|
||||
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
|
||||
}),
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
use assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
|
||||
use gpui::FocusHandle;
|
||||
use language_model_selector::{assistant_language_model_selector, LanguageModelSelector};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, PopoverMenuHandle};
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
pub selector: Entity<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
pub(crate) fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
fs,
|
||||
focus_handle,
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.menu_handle.toggle(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
AssistantLanguageModelSelector::new(self.focus_handle.clone(), self.selector.clone())
|
||||
.render(window, cx)
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let fs_clone = self.fs.clone();
|
||||
assistant_language_model_selector(
|
||||
self.focus_handle.clone(),
|
||||
Some(self.menu_handle.clone()),
|
||||
cx,
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ use editor::Editor;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, FontWeight, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
|
||||
FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
|
||||
WeakEntity,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
@@ -609,7 +610,7 @@ impl AssistantPanel {
|
||||
.id("title")
|
||||
.overflow_x_scroll()
|
||||
.px(DynamicSpacing::Base08.rems(cx))
|
||||
.child(Label::new(title).text_ellipsis()),
|
||||
.child(Label::new(title).truncate()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -993,12 +994,21 @@ impl AssistantPanel {
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AssistantPanel2");
|
||||
if matches!(self.active_view, ActiveView::PromptEditor) {
|
||||
key_context.add("prompt_editor");
|
||||
}
|
||||
key_context
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("AssistantPanel2")
|
||||
.key_context(self.key_context())
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
|
||||
@@ -20,6 +20,7 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use parking_lot::Mutex;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
@@ -102,11 +103,9 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.items_start()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
let selector = this.model_selector.read(cx).selector.clone();
|
||||
selector.update(cx, |selector, cx| {
|
||||
selector.toggle_model_selector(action, window, cx);
|
||||
})
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
|
||||
@@ -8,6 +8,7 @@ use gpui::{
|
||||
TextStyle, WeakEntity,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
@@ -297,11 +298,9 @@ impl Render for MessageEditor {
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
let selector = this.model_selector.read(cx).selector.clone();
|
||||
selector.update(cx, |this, cx| {
|
||||
this.toggle_model_selector(action, window, cx);
|
||||
})
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
|
||||
@@ -8,8 +8,9 @@ use futures::StreamExt as _;
|
||||
use gpui::{App, Context, EventEmitter, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolUseId,
|
||||
MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError, Role, StopReason,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
@@ -27,7 +28,7 @@ pub enum RequestKind {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct ThreadId(Arc<str>);
|
||||
pub struct ThreadId(pub Arc<str>);
|
||||
|
||||
impl ThreadId {
|
||||
pub fn new() -> Self {
|
||||
@@ -88,7 +89,7 @@ impl Thread {
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
tools,
|
||||
tool_use: ToolUseState::default(),
|
||||
tool_use: ToolUseState::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,14 @@ impl Thread {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let next_message_id = MessageId(saved.messages.len());
|
||||
let next_message_id = MessageId(
|
||||
saved
|
||||
.messages
|
||||
.last()
|
||||
.map(|message| message.id.0 + 1)
|
||||
.unwrap_or(0),
|
||||
);
|
||||
let tool_use = ToolUseState::from_saved_messages(&saved.messages);
|
||||
|
||||
Self {
|
||||
id,
|
||||
@@ -120,7 +128,7 @@ impl Thread {
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
tools,
|
||||
tool_use: ToolUseState::default(),
|
||||
tool_use,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +197,10 @@ impl Thread {
|
||||
self.tool_use.tool_uses_for_message(id)
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||
self.tool_use.tool_results_for_message(id)
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
@@ -223,6 +235,34 @@ impl Thread {
|
||||
id
|
||||
}
|
||||
|
||||
pub fn edit_message(
|
||||
&mut self,
|
||||
id: MessageId,
|
||||
new_role: Role,
|
||||
new_text: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
|
||||
return false;
|
||||
};
|
||||
message.role = new_role;
|
||||
message.text = new_text;
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageEdited(id));
|
||||
true
|
||||
}
|
||||
|
||||
pub fn delete_message(&mut self, id: MessageId, cx: &mut Context<Self>) -> bool {
|
||||
let Some(index) = self.messages.iter().position(|message| message.id == id) else {
|
||||
return false;
|
||||
};
|
||||
self.messages.remove(index);
|
||||
self.context_by_message.remove(&id);
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageDeleted(id));
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns the representation of this [`Thread`] in a textual form.
|
||||
///
|
||||
/// This is the representation we use when attaching a thread as context to another thread.
|
||||
@@ -561,6 +601,8 @@ pub enum ThreadEvent {
|
||||
StreamedCompletion,
|
||||
StreamedAssistantText(MessageId, String),
|
||||
MessageAdded(MessageId),
|
||||
MessageEdited(MessageId),
|
||||
MessageDeleted(MessageId),
|
||||
SummaryChanged,
|
||||
UsePendingTools,
|
||||
ToolFinished {
|
||||
|
||||
@@ -33,9 +33,9 @@ impl ThreadHistory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_prev(
|
||||
pub fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrev,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -166,7 +166,7 @@ impl Render for ThreadHistory {
|
||||
.overflow_y_scroll()
|
||||
.size_full()
|
||||
.p_1()
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
@@ -260,7 +260,7 @@ impl RenderOnce for PastThread {
|
||||
.start_slot(
|
||||
div()
|
||||
.max_w_4_5()
|
||||
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
|
||||
.child(Label::new(summary).size(LabelSize::Small).truncate()),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
@@ -356,7 +356,7 @@ impl RenderOnce for PastContext {
|
||||
.start_slot(
|
||||
div()
|
||||
.max_w_4_5()
|
||||
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
|
||||
.child(Label::new(summary).size(LabelSize::Small).truncate()),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
|
||||
@@ -12,9 +12,9 @@ use futures::FutureExt as _;
|
||||
use gpui::{
|
||||
prelude::*, App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task,
|
||||
};
|
||||
use heed::types::SerdeBincode;
|
||||
use heed::types::{SerdeBincode, SerdeJson};
|
||||
use heed::Database;
|
||||
use language_model::Role;
|
||||
use language_model::{LanguageModelToolUseId, Role};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt as _;
|
||||
@@ -113,6 +113,24 @@ impl ThreadStore {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text: message.text.clone(),
|
||||
tool_uses: thread
|
||||
.tool_uses_for_message(message.id)
|
||||
.into_iter()
|
||||
.map(|tool_use| SavedToolUse {
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
})
|
||||
.collect(),
|
||||
tool_results: thread
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.map(|tool_result| SavedToolResult {
|
||||
tool_use_id: tool_result.tool_use_id.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
@@ -239,11 +257,29 @@ pub struct SavedThread {
|
||||
pub messages: Vec<SavedMessage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub role: Role,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub tool_uses: Vec<SavedToolUse>,
|
||||
#[serde(default)]
|
||||
pub tool_results: Vec<SavedToolResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: SharedString,
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedToolResult {
|
||||
pub tool_use_id: LanguageModelToolUseId,
|
||||
pub is_error: bool,
|
||||
pub content: Arc<str>,
|
||||
}
|
||||
|
||||
struct GlobalThreadsDatabase(
|
||||
@@ -255,7 +291,7 @@ impl Global for GlobalThreadsDatabase {}
|
||||
pub(crate) struct ThreadsDatabase {
|
||||
executor: BackgroundExecutor,
|
||||
env: heed::Env,
|
||||
threads: Database<SerdeBincode<ThreadId>, SerdeBincode<SavedThread>>,
|
||||
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SavedThread>>,
|
||||
}
|
||||
|
||||
impl ThreadsDatabase {
|
||||
@@ -270,7 +306,7 @@ impl ThreadsDatabase {
|
||||
let database_future = executor
|
||||
.spawn({
|
||||
let executor = executor.clone();
|
||||
let database_path = paths::support_dir().join("threads/threads-db.0.mdb");
|
||||
let database_path = paths::support_dir().join("threads/threads-db.1.mdb");
|
||||
async move { ThreadsDatabase::new(database_path, executor) }
|
||||
})
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
|
||||
@@ -7,10 +7,11 @@ use futures::FutureExt as _;
|
||||
use gpui::{SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent,
|
||||
LanguageModelToolUseId, MessageContent, Role,
|
||||
};
|
||||
|
||||
use crate::thread::MessageId;
|
||||
use crate::thread_store::SavedMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolUse {
|
||||
@@ -28,7 +29,6 @@ pub enum ToolUseStatus {
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ToolUseState {
|
||||
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
|
||||
@@ -37,6 +37,65 @@ pub struct ToolUseState {
|
||||
}
|
||||
|
||||
impl ToolUseState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tool_uses_by_assistant_message: HashMap::default(),
|
||||
tool_uses_by_user_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_saved_messages(messages: &[SavedMessage]) -> Self {
|
||||
let mut this = Self::new();
|
||||
|
||||
for message in messages {
|
||||
match message.role {
|
||||
Role::Assistant => {
|
||||
if !message.tool_uses.is_empty() {
|
||||
this.tool_uses_by_assistant_message.insert(
|
||||
message.id,
|
||||
message
|
||||
.tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
input: tool_use.input.clone(),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Role::User => {
|
||||
if !message.tool_results.is_empty() {
|
||||
let tool_uses_by_user_message = this
|
||||
.tool_uses_by_user_message
|
||||
.entry(message.id)
|
||||
.or_default();
|
||||
|
||||
for tool_result in &message.tool_results {
|
||||
let tool_use_id = tool_result.tool_use_id.clone();
|
||||
|
||||
tool_uses_by_user_message.push(tool_use_id.clone());
|
||||
this.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id,
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Role::System => {}
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
@@ -84,6 +143,17 @@ impl ToolUseState {
|
||||
tool_uses
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||
let empty = Vec::new();
|
||||
|
||||
self.tool_uses_by_user_message
|
||||
.get(&message_id)
|
||||
.unwrap_or(&empty)
|
||||
.iter()
|
||||
.filter_map(|tool_use_id| self.tool_results.get(&tool_use_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_user_message
|
||||
.get(&message_id)
|
||||
|
||||
@@ -37,7 +37,9 @@ use language_model::{
|
||||
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
|
||||
use language_model_selector::{
|
||||
assistant_language_model_selector, LanguageModelSelector, ToggleModelSelector,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::Picker;
|
||||
use project::lsp_store::LocalLspAdapterDelegate;
|
||||
@@ -52,7 +54,7 @@ use ui::{
|
||||
Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::searchable::SearchableItemHandle;
|
||||
use workspace::searchable::{Direction, SearchableItemHandle};
|
||||
use workspace::{
|
||||
item::{self, FollowableItem, Item, ItemHandle},
|
||||
notifications::NotificationId,
|
||||
@@ -195,7 +197,7 @@ pub struct ContextEditor {
|
||||
// the file is opened. In order to keep the worktree alive for the duration of the
|
||||
// context editor, we keep a reference here.
|
||||
dragged_file_worktrees: Vec<Entity<Worktree>>,
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
language_model_selector: PopoverMenuHandle<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_TAB_TITLE: &str = "New Chat";
|
||||
@@ -249,21 +251,6 @@ impl ContextEditor {
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
];
|
||||
|
||||
let fs_clone = fs.clone();
|
||||
let language_model_selector = cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let sections = context.read(cx).slash_command_output_sections().to_vec();
|
||||
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
|
||||
let slash_commands = context.read(cx).slash_commands().clone();
|
||||
@@ -288,7 +275,7 @@ impl ContextEditor {
|
||||
show_accept_terms: false,
|
||||
slash_menu_handle: Default::default(),
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
language_model_selector,
|
||||
language_model_selector: PopoverMenuHandle::default(),
|
||||
};
|
||||
this.update_message_headers(cx);
|
||||
this.update_image_blocks(cx);
|
||||
@@ -2831,6 +2818,7 @@ impl Render for ContextEditor {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let fs_clone = self.fs.clone();
|
||||
|
||||
let language_model_selector = self.language_model_selector.clone();
|
||||
v_flex()
|
||||
@@ -2845,10 +2833,8 @@ impl Render for ContextEditor {
|
||||
.on_action(cx.listener(ContextEditor::edit))
|
||||
.on_action(cx.listener(ContextEditor::assist))
|
||||
.on_action(cx.listener(ContextEditor::split))
|
||||
.on_action(move |action, window, cx| {
|
||||
language_model_selector.update(cx, |this, cx| {
|
||||
this.toggle_model_selector(action, window, cx);
|
||||
})
|
||||
.on_action(move |_: &ToggleModelSelector, window, cx| {
|
||||
language_model_selector.toggle(window, cx);
|
||||
})
|
||||
.size_full()
|
||||
.children(self.render_notice(cx))
|
||||
@@ -2887,14 +2873,18 @@ impl Render for ContextEditor {
|
||||
.gap_1()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.child(ui::Divider::vertical())
|
||||
.child(div().pl_0p5().child({
|
||||
let focus_handle = self.editor().focus_handle(cx).clone();
|
||||
AssistantLanguageModelSelector::new(
|
||||
focus_handle,
|
||||
self.language_model_selector.clone(),
|
||||
)
|
||||
.render(window, cx)
|
||||
})),
|
||||
.child(div().pl_0p5().child(assistant_language_model_selector(
|
||||
self.editor().focus_handle(cx),
|
||||
Some(self.language_model_selector.clone()),
|
||||
cx,
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
))),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -3070,12 +3060,13 @@ impl SearchableItem for ContextEditor {
|
||||
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
matches: &[Self::Match],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.active_match_index(matches, window, cx)
|
||||
editor.active_match_index(direction, matches, window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,24 +207,31 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(info.icon).size(IconSize::XSmall))
|
||||
.child(div().font_buffer(cx).child({
|
||||
.child(
|
||||
Icon::new(info.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child({
|
||||
let mut label = format!("{}", info.name);
|
||||
if let Some(args) = info.args.as_ref().filter(|_| selected)
|
||||
{
|
||||
label.push_str(&args);
|
||||
}
|
||||
Label::new(label).single_line().size(LabelSize::Small)
|
||||
}))
|
||||
Label::new(label)
|
||||
.single_line()
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
})
|
||||
.children(info.args.clone().filter(|_| !selected).map(
|
||||
|args| {
|
||||
div()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args)
|
||||
.single_line()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
@@ -236,7 +243,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.text_ellipsis(),
|
||||
.truncate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -294,10 +301,9 @@ where
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
|
||||
.child(
|
||||
div().font_buffer(cx).child(
|
||||
Label::new("create-your-command")
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
Label::new("create-your-command")
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -341,7 +347,7 @@ where
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
.when_some(handle, |this, handle| this.with_handle(handle))
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
@@ -4,7 +4,16 @@ mod tool_working_set;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::AnyElement;
|
||||
use gpui::IntoElement;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use language::Language;
|
||||
use ui::div;
|
||||
use ui::Label;
|
||||
use ui::LabelCommon;
|
||||
use ui::LabelSize;
|
||||
use ui::ParentElement;
|
||||
use ui::SharedString;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::tool_registry::*;
|
||||
@@ -31,8 +40,52 @@ pub trait Tool: 'static + Send + Sync {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
thread_id: Arc<str>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>>;
|
||||
|
||||
/// Renders the tool's input when the user expands it.
|
||||
fn render_input(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_lua_language: Option<Arc<Language>>,
|
||||
_cx: &mut App,
|
||||
) -> AnyElement {
|
||||
default_render_input(input)
|
||||
}
|
||||
|
||||
/// Renders the tool's output when the user expands it.
|
||||
fn render_output(self: Arc<Self>, output: SharedString, _cx: &mut App) -> AnyElement {
|
||||
default_render_output(output)
|
||||
}
|
||||
|
||||
/// Renders the tool's error message when the user expands it.
|
||||
fn render_error(self: Arc<Self>, err: SharedString, _cx: &mut App) -> AnyElement {
|
||||
default_render_error(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_render_input(input: serde_json::Value) -> AnyElement {
|
||||
div()
|
||||
.child(Label::new("Input:").size(LabelSize::Small))
|
||||
.child(Label::new(
|
||||
serde_json::to_string_pretty(&input).unwrap_or_default(),
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn default_render_output(output: SharedString) -> AnyElement {
|
||||
div()
|
||||
.child(Label::new("Result:").size(LabelSize::Small))
|
||||
.child(Label::new(output))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn default_render_error(err: SharedString) -> AnyElement {
|
||||
div()
|
||||
.child(Label::new("Error:").size(LabelSize::Small))
|
||||
.child(Label::new(err))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -41,6 +40,7 @@ impl Tool for NowTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_thread_id: Arc<str>,
|
||||
_workspace: WeakEntity<workspace::Workspace>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
|
||||
@@ -141,19 +141,20 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
|
||||
cx,
|
||||
move |cx| {
|
||||
let workspace_handle = cx.entity().downgrade();
|
||||
cx.new(|_cx| {
|
||||
MessageNotification::new(format!("Updated to {app_name} {}", version))
|
||||
.primary_message("View Release Notes")
|
||||
.primary_on_click(move |window, cx| {
|
||||
if let Some(workspace) = workspace_handle.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
crate::view_release_notes_locally(
|
||||
workspace, window, cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
cx.new(|cx| {
|
||||
MessageNotification::new(
|
||||
format!("Updated to {app_name} {}", version),
|
||||
cx,
|
||||
)
|
||||
.primary_message("View Release Notes")
|
||||
.primary_on_click(move |window, cx| {
|
||||
if let Some(workspace) = workspace_handle.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
crate::view_release_notes_locally(workspace, window, cx);
|
||||
})
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
@@ -82,7 +82,7 @@ impl Render for Breadcrumbs {
|
||||
text_style.color = Color::Muted.color(cx);
|
||||
|
||||
StyledText::new(segment.text.replace('\n', "⏎"))
|
||||
.with_highlights(&text_style, segment.highlights.unwrap_or_default())
|
||||
.with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
|
||||
.into_any()
|
||||
});
|
||||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||
|
||||
@@ -56,8 +56,8 @@ pub enum DiffHunkSecondaryStatus {
|
||||
/// A diff hunk resolved to rows in the buffer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DiffHunk {
|
||||
/// The buffer range, expressed in terms of rows.
|
||||
pub row_range: Range<u32>,
|
||||
/// The buffer range as points.
|
||||
pub range: Range<Point>,
|
||||
/// The range in the buffer to which this hunk corresponds.
|
||||
pub buffer_range: Range<Anchor>,
|
||||
/// The range in the buffer's diff base text to which this hunk corresponds.
|
||||
@@ -362,6 +362,7 @@ impl BufferDiffInner {
|
||||
pending_hunks = secondary.pending_hunks.clone();
|
||||
}
|
||||
|
||||
let max_point = buffer.max_point();
|
||||
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
|
||||
iter::from_fn(move || loop {
|
||||
let (start_point, (start_anchor, start_base)) = summaries.next()?;
|
||||
@@ -371,7 +372,7 @@ impl BufferDiffInner {
|
||||
continue;
|
||||
}
|
||||
|
||||
if end_point.column > 0 {
|
||||
if end_point.column > 0 && end_point < max_point {
|
||||
end_point.row += 1;
|
||||
end_point.column = 0;
|
||||
end_anchor = buffer.anchor_before(end_point);
|
||||
@@ -416,7 +417,7 @@ impl BufferDiffInner {
|
||||
}
|
||||
|
||||
return Some(DiffHunk {
|
||||
row_range: start_point.row..end_point.row,
|
||||
range: start_point..end_point,
|
||||
diff_base_byte_range: start_base..end_base,
|
||||
buffer_range: start_anchor..end_anchor,
|
||||
secondary_status,
|
||||
@@ -442,14 +443,9 @@ impl BufferDiffInner {
|
||||
|
||||
let hunk = cursor.item()?;
|
||||
let range = hunk.buffer_range.to_point(buffer);
|
||||
let end_row = if range.end.column > 0 {
|
||||
range.end.row + 1
|
||||
} else {
|
||||
range.end.row
|
||||
};
|
||||
|
||||
Some(DiffHunk {
|
||||
row_range: range.start.row..end_row,
|
||||
range,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
// The secondary status is not used by callers of this method.
|
||||
@@ -1136,12 +1132,10 @@ pub fn assert_hunks<Iter>(
|
||||
let actual_hunks = diff_hunks
|
||||
.map(|hunk| {
|
||||
(
|
||||
hunk.row_range.clone(),
|
||||
hunk.range.clone(),
|
||||
&diff_base[hunk.diff_base_byte_range.clone()],
|
||||
buffer
|
||||
.text_for_range(
|
||||
Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
|
||||
)
|
||||
.text_for_range(hunk.range.clone())
|
||||
.collect::<String>(),
|
||||
hunk.status(),
|
||||
)
|
||||
@@ -1150,7 +1144,14 @@ pub fn assert_hunks<Iter>(
|
||||
|
||||
let expected_hunks: Vec<_> = expected_hunks
|
||||
.iter()
|
||||
.map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status))
|
||||
.map(|(r, old_text, new_text, status)| {
|
||||
(
|
||||
Point::new(r.start, 0)..Point::new(r.end, 0),
|
||||
*old_text,
|
||||
new_text.to_string(),
|
||||
*status,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(actual_hunks, expected_hunks);
|
||||
|
||||
@@ -614,12 +614,19 @@ mod windows {
|
||||
let path = if let Some(path) = path {
|
||||
path.to_path_buf().canonicalize()?
|
||||
} else {
|
||||
std::env::current_exe()?
|
||||
.parent()
|
||||
.context("no parent path for cli")?
|
||||
.parent()
|
||||
.context("no parent path for cli folder")?
|
||||
.join("Zed.exe")
|
||||
let cli = std::env::current_exe()?;
|
||||
let dir = cli.parent().context("no parent path for cli")?;
|
||||
|
||||
// ../Zed.exe is the standard, lib/zed is for MSYS2, ./zed.exe is for the target
|
||||
// directory in development builds.
|
||||
let possible_locations = ["../Zed.exe", "../lib/zed/zed-editor.exe", "./zed.exe"];
|
||||
possible_locations
|
||||
.iter()
|
||||
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
|
||||
.context(format!(
|
||||
"could not find any of: {}",
|
||||
possible_locations.join(", ")
|
||||
))?
|
||||
};
|
||||
|
||||
Ok(App(path))
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui::{
|
||||
ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, SharedString,
|
||||
Styled, Subscription, Task, TextStyle, WeakEntity, Window,
|
||||
};
|
||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
|
||||
use project::{Fs, Project};
|
||||
use rpc::{
|
||||
proto::{self, ChannelVisibility, PeerId},
|
||||
@@ -1430,7 +1430,7 @@ impl CollabPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, _: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = self.selection.take().unwrap_or(0);
|
||||
if ix > 0 {
|
||||
self.selection = Some(ix - 1);
|
||||
@@ -2878,7 +2878,7 @@ impl Render for CollabPanel {
|
||||
.key_context("CollabPanel")
|
||||
.on_action(cx.listener(CollabPanel::cancel))
|
||||
.on_action(cx.listener(CollabPanel::select_next))
|
||||
.on_action(cx.listener(CollabPanel::select_prev))
|
||||
.on_action(cx.listener(CollabPanel::select_previous))
|
||||
.on_action(cx.listener(CollabPanel::confirm))
|
||||
.on_action(cx.listener(CollabPanel::insert_space))
|
||||
.on_action(cx.listener(CollabPanel::remove_selected_channel))
|
||||
|
||||
@@ -22,7 +22,7 @@ use ui::{
|
||||
h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::notifications::{Notification as WorkspaceNotification, NotificationId};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
Workspace,
|
||||
@@ -570,11 +570,12 @@ impl NotificationPanel {
|
||||
workspace.dismiss_notification(&id, cx);
|
||||
workspace.show_notification(id, cx, |cx| {
|
||||
let workspace = cx.entity().downgrade();
|
||||
cx.new(|_| NotificationToast {
|
||||
cx.new(|cx| NotificationToast {
|
||||
notification_id,
|
||||
actor,
|
||||
text,
|
||||
workspace,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -771,8 +772,17 @@ pub struct NotificationToast {
|
||||
actor: Option<Arc<User>>,
|
||||
text: String,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for NotificationToast {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceNotification for NotificationToast {}
|
||||
|
||||
impl NotificationToast {
|
||||
fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
@@ -226,3 +226,7 @@ impl Item for ComponentPreview {
|
||||
f(*event)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: impl serializable item for component preview so it will restore with the workspace
|
||||
// ref: https://github.com/zed-industries/zed/blob/32201ac70a501e63dfa2ade9c00f85aea2d4dd94/crates/image_viewer/src/image_viewer.rs#L199
|
||||
// Use `ImageViewer` as a model for how to do it, except it'll be even simpler
|
||||
|
||||
@@ -51,6 +51,7 @@ impl Tool for ContextServerTool {
|
||||
fn run(
|
||||
self: std::sync::Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_thread_id: Arc<str>,
|
||||
_workspace: gpui::WeakEntity<workspace::Workspace>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
|
||||
@@ -995,7 +995,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
StyledText::new(message.clone()).with_highlights(
|
||||
StyledText::new(message.clone()).with_default_highlights(
|
||||
&cx.window.text_style(),
|
||||
code_ranges
|
||||
.iter()
|
||||
|
||||
@@ -35,6 +35,13 @@ pub struct SelectToBeginningOfLine {
|
||||
pub stop_at_indent: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DeleteToBeginningOfLine {
|
||||
#[serde(default)]
|
||||
pub(super) stop_at_indent: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MovePageUp {
|
||||
@@ -212,6 +219,7 @@ impl_actions!(
|
||||
ComposeCompletion,
|
||||
ConfirmCodeAction,
|
||||
ConfirmCompletion,
|
||||
DeleteToBeginningOfLine,
|
||||
DeleteToNextWordEnd,
|
||||
DeleteToPreviousWordStart,
|
||||
ExpandExcerpts,
|
||||
@@ -257,7 +265,7 @@ gpui::actions!(
|
||||
ContextMenuFirst,
|
||||
ContextMenuLast,
|
||||
ContextMenuNext,
|
||||
ContextMenuPrev,
|
||||
ContextMenuPrevious,
|
||||
ConvertToKebabCase,
|
||||
ConvertToLowerCamelCase,
|
||||
ConvertToLowerCase,
|
||||
@@ -276,7 +284,6 @@ gpui::actions!(
|
||||
CutToEndOfLine,
|
||||
Delete,
|
||||
DeleteLine,
|
||||
DeleteToBeginningOfLine,
|
||||
DeleteToEndOfLine,
|
||||
DeleteToNextSubwordEnd,
|
||||
DeleteToPreviousSubwordStart,
|
||||
@@ -301,10 +308,10 @@ gpui::actions!(
|
||||
GoToDefinitionSplit,
|
||||
GoToDiagnostic,
|
||||
GoToHunk,
|
||||
GoToPreviousHunk,
|
||||
GoToImplementation,
|
||||
GoToImplementationSplit,
|
||||
GoToPrevDiagnostic,
|
||||
GoToPrevHunk,
|
||||
GoToPreviousDiagnostic,
|
||||
GoToTypeDefinition,
|
||||
GoToTypeDefinitionSplit,
|
||||
HalfPageDown,
|
||||
@@ -399,7 +406,7 @@ gpui::actions!(
|
||||
SplitSelectionIntoLines,
|
||||
SwitchSourceHeader,
|
||||
Tab,
|
||||
TabPrev,
|
||||
Backtab,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
|
||||
@@ -514,7 +514,7 @@ impl CompletionsMenu {
|
||||
);
|
||||
|
||||
let completion_label = StyledText::new(completion.label.text.clone())
|
||||
.with_highlights(&style.text, highlights);
|
||||
.with_default_highlights(&style.text, highlights);
|
||||
let documentation_label = if let Some(
|
||||
CompletionDocumentation::SingleLine(text),
|
||||
) = documentation
|
||||
|
||||
@@ -43,7 +43,7 @@ use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, Under
|
||||
pub use inlay_map::Inlay;
|
||||
use inlay_map::{InlayMap, InlaySnapshot};
|
||||
pub use inlay_map::{InlayOffset, InlayPoint};
|
||||
use invisibles::{is_invisible, replacement};
|
||||
pub use invisibles::{is_invisible, replacement};
|
||||
use language::{
|
||||
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
|
||||
Subscription as BufferSubscription,
|
||||
@@ -1124,6 +1124,11 @@ impl DisplaySnapshot {
|
||||
self.block_snapshot.is_block_line(BlockRow(display_row.0))
|
||||
}
|
||||
|
||||
pub fn is_folded_buffer_header(&self, display_row: DisplayRow) -> bool {
|
||||
self.block_snapshot
|
||||
.is_folded_buffer_header(BlockRow(display_row.0))
|
||||
}
|
||||
|
||||
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
|
||||
let wrap_row = self
|
||||
.block_snapshot
|
||||
|
||||
@@ -1618,6 +1618,15 @@ impl BlockSnapshot {
|
||||
cursor.item().map_or(false, |t| t.block.is_some())
|
||||
}
|
||||
|
||||
pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&row, Bias::Right, &());
|
||||
let Some(transform) = cursor.item() else {
|
||||
return false;
|
||||
};
|
||||
matches!(transform.block, Some(Block::FoldedBuffer { .. }))
|
||||
}
|
||||
|
||||
pub(super) fn is_line_replaced(&self, row: MultiBufferRow) -> bool {
|
||||
let wrap_point = self
|
||||
.wrap_snapshot
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn is_invisible(c: char) -> bool {
|
||||
// ASCII control characters have fancy unicode glyphs, everything else
|
||||
// is replaced by a space - unless it is used in combining characters in
|
||||
// which case we need to leave it in the string.
|
||||
pub(crate) fn replacement(c: char) -> Option<&'static str> {
|
||||
pub fn replacement(c: char) -> Option<&'static str> {
|
||||
if c <= '\x1f' {
|
||||
Some(C0_SYMBOLS[c as usize])
|
||||
} else if c == '\x7f' {
|
||||
|
||||
@@ -73,7 +73,7 @@ use futures::{
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
|
||||
use ::git::{status::FileStatus, Restore};
|
||||
use ::git::Restore;
|
||||
use code_context_menus::{
|
||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||
CompletionsMenu, ContextMenuOrigin,
|
||||
@@ -85,8 +85,8 @@ use gpui::{
|
||||
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
|
||||
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
|
||||
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
|
||||
ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
|
||||
Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
|
||||
WeakEntity, WeakFocusHandle, Window,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
@@ -2233,6 +2233,43 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn sync_selections(
|
||||
&mut self,
|
||||
other: Entity<Editor>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::Subscription {
|
||||
let other_selections = other.read(cx).selections.disjoint.to_vec();
|
||||
self.selections.change_with(cx, |selections| {
|
||||
selections.select_anchors(other_selections);
|
||||
});
|
||||
|
||||
let other_subscription =
|
||||
cx.subscribe(&other, |this, other, other_evt, cx| match other_evt {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let other_selections = other.read(cx).selections.disjoint.to_vec();
|
||||
this.selections.change_with(cx, |selections| {
|
||||
selections.select_anchors(other_selections);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let this_subscription =
|
||||
cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| match this_evt {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let these_selections = this.selections.disjoint.to_vec();
|
||||
other.update(cx, |other_editor, cx| {
|
||||
other_editor.selections.change_with(cx, |selections| {
|
||||
selections.select_anchors(these_selections);
|
||||
})
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Subscription::join(other_subscription, this_subscription)
|
||||
}
|
||||
|
||||
pub fn change_selections<R>(
|
||||
&mut self,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
@@ -2877,7 +2914,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
if let Some(bracket_pair) = bracket_pair {
|
||||
let snapshot_settings = snapshot.settings_at(selection.start, cx);
|
||||
let snapshot_settings = snapshot.language_settings_at(selection.start, cx);
|
||||
let autoclose = self.use_autoclose && snapshot_settings.use_autoclose;
|
||||
let auto_surround =
|
||||
self.use_auto_surround && snapshot_settings.use_auto_surround;
|
||||
@@ -2942,7 +2979,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let always_treat_brackets_as_autoclosed = snapshot
|
||||
.settings_at(selection.start, cx)
|
||||
.language_settings_at(selection.start, cx)
|
||||
.always_treat_brackets_as_autoclosed;
|
||||
if always_treat_brackets_as_autoclosed
|
||||
&& is_bracket_pair_end
|
||||
@@ -3225,7 +3262,7 @@ impl Editor {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.settings_at(0, cx).extend_comment_on_newline {
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -3554,7 +3591,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let always_treat_brackets_as_autoclosed = buffer
|
||||
.settings_at(selection.start, cx)
|
||||
.language_settings_at(selection.start, cx)
|
||||
.always_treat_brackets_as_autoclosed;
|
||||
|
||||
if !always_treat_brackets_as_autoclosed {
|
||||
@@ -3691,6 +3728,7 @@ impl Editor {
|
||||
InlayHintRefreshReason::SettingsChange(_)
|
||||
| InlayHintRefreshReason::Toggle(_)
|
||||
| InlayHintRefreshReason::ExcerptsRemoved(_)
|
||||
| InlayHintRefreshReason::ModifiersChanged(_)
|
||||
);
|
||||
let (invalidate_cache, required_languages) = match reason {
|
||||
InlayHintRefreshReason::ModifiersChanged(enabled) => {
|
||||
@@ -6293,6 +6331,9 @@ impl Editor {
|
||||
|
||||
const BORDER_WIDTH: Pixels = px(1.);
|
||||
|
||||
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
|
||||
let has_keybind = keybind.is_some();
|
||||
|
||||
let mut element = h_flex()
|
||||
.items_start()
|
||||
.child(
|
||||
@@ -6323,7 +6364,19 @@ impl Editor {
|
||||
.border(BORDER_WIDTH)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_r_lg()
|
||||
.children(self.render_edit_prediction_accept_keybind(window, cx)),
|
||||
.id("edit_prediction_diff_popover_keybind")
|
||||
.when(!has_keybind, |el| {
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
el.bg(status_colors.error_background)
|
||||
.border_color(status_colors.error.opacity(0.6))
|
||||
.child(Icon::new(IconName::Info).color(Color::Error))
|
||||
.cursor_default()
|
||||
.hoverable_tooltip(move |_window, cx| {
|
||||
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
|
||||
})
|
||||
})
|
||||
.children(keybind),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
@@ -6421,7 +6474,11 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_edit_prediction_accept_keybind(&self, window: &mut Window, cx: &App) -> Option<Div> {
|
||||
fn render_edit_prediction_accept_keybind(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> Option<AnyElement> {
|
||||
let accept_binding = self.accept_edit_prediction_keybind(window, cx);
|
||||
let accept_keystroke = accept_binding.keystroke()?;
|
||||
|
||||
@@ -6457,6 +6514,7 @@ impl Editor {
|
||||
.size(Some(IconSize::XSmall.rems().into())),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -6466,10 +6524,14 @@ impl Editor {
|
||||
icon: Option<IconName>,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> Option<Div> {
|
||||
) -> Option<Stateful<Div>> {
|
||||
let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
|
||||
|
||||
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
|
||||
let has_keybind = keybind.is_some();
|
||||
|
||||
let result = h_flex()
|
||||
.id("ep-line-popover")
|
||||
.py_0p5()
|
||||
.pl_1()
|
||||
.pr(padding_right)
|
||||
@@ -6479,8 +6541,35 @@ impl Editor {
|
||||
.bg(Self::edit_prediction_line_popover_bg_color(cx))
|
||||
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
|
||||
.shadow_sm()
|
||||
.children(self.render_edit_prediction_accept_keybind(window, cx))
|
||||
.child(Label::new(label).size(LabelSize::Small))
|
||||
.when(!has_keybind, |el| {
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
el.bg(status_colors.error_background)
|
||||
.border_color(status_colors.error.opacity(0.6))
|
||||
.pl_2()
|
||||
.child(Icon::new(IconName::ZedPredictError).color(Color::Error))
|
||||
.cursor_default()
|
||||
.hoverable_tooltip(move |_window, cx| {
|
||||
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
|
||||
})
|
||||
})
|
||||
.children(keybind)
|
||||
.child(
|
||||
Label::new(label)
|
||||
.size(LabelSize::Small)
|
||||
.when(!has_keybind, |el| {
|
||||
el.color(cx.theme().status().error.into()).strikethrough()
|
||||
}),
|
||||
)
|
||||
.when(!has_keybind, |el| {
|
||||
el.child(
|
||||
h_flex().ml_1().child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Small)
|
||||
.color(cx.theme().status().error.into()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when_some(icon, |element, icon| {
|
||||
element.child(
|
||||
div()
|
||||
@@ -6577,6 +6666,9 @@ impl Editor {
|
||||
.elevation_2(cx)
|
||||
.border(BORDER_WIDTH)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.when(accept_keystroke.is_none(), |el| {
|
||||
el.border_color(cx.theme().status().error)
|
||||
})
|
||||
.rounded(RADIUS)
|
||||
.rounded_tl(px(0.))
|
||||
.overflow_hidden()
|
||||
@@ -6605,16 +6697,37 @@ impl Editor {
|
||||
el.child(
|
||||
Label::new("Hold")
|
||||
.size(LabelSize::Small)
|
||||
.when(accept_keystroke.is_none(), |el| {
|
||||
el.strikethrough()
|
||||
})
|
||||
.line_height_style(LineHeightStyle::UiLabel),
|
||||
)
|
||||
})
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke?.modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(Color::Default),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
false,
|
||||
))),
|
||||
.id("edit_prediction_cursor_popover_keybind")
|
||||
.when(accept_keystroke.is_none(), |el| {
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
el.bg(status_colors.error_background)
|
||||
.border_color(status_colors.error.opacity(0.6))
|
||||
.child(Icon::new(IconName::Info).color(Color::Error))
|
||||
.cursor_default()
|
||||
.hoverable_tooltip(move |_window, cx| {
|
||||
cx.new(|_| MissingEditPredictionKeybindingTooltip)
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.when_some(
|
||||
accept_keystroke.as_ref(),
|
||||
|el, accept_keystroke| {
|
||||
el.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(Color::Default),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
false,
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.into_any(),
|
||||
);
|
||||
@@ -6780,7 +6893,7 @@ impl Editor {
|
||||
.first_line_preview();
|
||||
|
||||
let styled_text = gpui::StyledText::new(highlighted_edits.text)
|
||||
.with_highlights(&style.text, highlighted_edits.highlights);
|
||||
.with_default_highlights(&style.text, highlighted_edits.highlights);
|
||||
|
||||
let preview = h_flex()
|
||||
.gap_1()
|
||||
@@ -7191,7 +7304,7 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.move_to_prev_snippet_tabstop(window, cx) {
|
||||
return;
|
||||
}
|
||||
@@ -7252,7 +7365,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
// Otherwise, insert a hard or soft tab.
|
||||
let settings = buffer.settings_at(cursor, cx);
|
||||
let settings = buffer.language_settings_at(cursor, cx);
|
||||
let tab_size = if settings.hard_tabs {
|
||||
IndentSize::tab()
|
||||
} else {
|
||||
@@ -7316,7 +7429,7 @@ impl Editor {
|
||||
delta_for_start_row: u32,
|
||||
cx: &App,
|
||||
) -> u32 {
|
||||
let settings = buffer.settings_at(selection.start, cx);
|
||||
let settings = buffer.language_settings_at(selection.start, cx);
|
||||
let tab_size = settings.tab_size.get();
|
||||
let indent_kind = if settings.hard_tabs {
|
||||
IndentKind::Tab
|
||||
@@ -7396,7 +7509,7 @@ impl Editor {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
for selection in &selections {
|
||||
let settings = buffer.settings_at(selection.start, cx);
|
||||
let settings = buffer.language_settings_at(selection.start, cx);
|
||||
let tab_size = settings.tab_size.get();
|
||||
let mut rows = selection.spanned_rows(false, &display_map);
|
||||
|
||||
@@ -8408,7 +8521,7 @@ impl Editor {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tab_size = buffer.settings_at(selection.head(), cx).tab_size;
|
||||
let tab_size = buffer.language_settings_at(selection.head(), cx).tab_size;
|
||||
|
||||
// Since not all lines in the selection may be at the same indent
|
||||
// level, choose the indent size that is the most common between all
|
||||
@@ -8454,7 +8567,7 @@ impl Editor {
|
||||
inside_comment = true;
|
||||
}
|
||||
|
||||
let language_settings = buffer.settings_at(selection.head(), cx);
|
||||
let language_settings = buffer.language_settings_at(selection.head(), cx);
|
||||
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
|
||||
RewrapBehavior::InComments => inside_comment,
|
||||
RewrapBehavior::InSelections => !selection.is_empty(),
|
||||
@@ -8510,7 +8623,7 @@ impl Editor {
|
||||
};
|
||||
|
||||
let wrap_column = buffer
|
||||
.settings_at(Point::new(start_row, 0), cx)
|
||||
.language_settings_at(Point::new(start_row, 0), cx)
|
||||
.preferred_line_length as usize;
|
||||
let wrapped_text = wrap_with_prefix(
|
||||
line_prefix,
|
||||
@@ -8696,8 +8809,9 @@ impl Editor {
|
||||
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
let snapshot = buffer.read(cx);
|
||||
auto_indent_on_paste =
|
||||
snapshot.settings_at(cursor_offset, cx).auto_indent_on_paste;
|
||||
auto_indent_on_paste = snapshot
|
||||
.language_settings_at(cursor_offset, cx)
|
||||
.auto_indent_on_paste;
|
||||
|
||||
let mut start_offset = 0;
|
||||
let mut edits = Vec::new();
|
||||
@@ -9235,7 +9349,7 @@ impl Editor {
|
||||
|
||||
pub fn context_menu_prev(
|
||||
&mut self,
|
||||
_: &ContextMenuPrev,
|
||||
_: &ContextMenuPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -9515,7 +9629,7 @@ impl Editor {
|
||||
|
||||
pub fn delete_to_beginning_of_line(
|
||||
&mut self,
|
||||
_: &DeleteToBeginningOfLine,
|
||||
action: &DeleteToBeginningOfLine,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -9529,7 +9643,7 @@ impl Editor {
|
||||
this.select_to_beginning_of_line(
|
||||
&SelectToBeginningOfLine {
|
||||
stop_at_soft_wraps: false,
|
||||
stop_at_indent: false,
|
||||
stop_at_indent: action.stop_at_indent,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
@@ -11262,7 +11376,7 @@ impl Editor {
|
||||
|
||||
fn go_to_prev_diagnostic(
|
||||
&mut self,
|
||||
_: &GoToPrevDiagnostic,
|
||||
_: &GoToPreviousDiagnostic,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -11414,28 +11528,39 @@ impl Editor {
|
||||
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_after_position(&snapshot, selection.head(), window, cx);
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
Direction::Next,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn go_to_hunk_after_position(
|
||||
fn go_to_hunk_after_or_before_position(
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
direction: Direction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
let hunk = self.hunk_after_position(snapshot, position);
|
||||
) {
|
||||
let row = if direction == Direction::Next {
|
||||
self.hunk_after_position(snapshot, position)
|
||||
.map(|hunk| hunk.row_range.start)
|
||||
} else {
|
||||
self.hunk_before_position(snapshot, position)
|
||||
};
|
||||
|
||||
if let Some(hunk) = &hunk {
|
||||
let point = Point::new(hunk.row_range.start.0, 0);
|
||||
if let Some(row) = row {
|
||||
let destination = Point::new(row.0, 0);
|
||||
let autoscroll = Autoscroll::center();
|
||||
|
||||
self.unfold_ranges(&[point..point], false, false, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.select_ranges([point..point]);
|
||||
self.unfold_ranges(&[destination..destination], false, false, cx);
|
||||
self.change_selections(Some(autoscroll), window, cx, |s| {
|
||||
s.select_ranges([destination..destination]);
|
||||
});
|
||||
}
|
||||
|
||||
hunk
|
||||
}
|
||||
|
||||
fn hunk_after_position(
|
||||
@@ -11455,32 +11580,32 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn go_to_prev_hunk(
|
||||
&mut self,
|
||||
_: &GoToPreviousHunk,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_before_position(&snapshot, selection.head(), window, cx);
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
Direction::Prev,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn go_to_hunk_before_position(
|
||||
fn hunk_before_position(
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
let mut hunk = snapshot.buffer_snapshot.diff_hunk_before(position);
|
||||
if hunk.is_none() {
|
||||
hunk = snapshot.buffer_snapshot.diff_hunk_before(Point::MAX);
|
||||
}
|
||||
if let Some(hunk) = &hunk {
|
||||
let destination = Point::new(hunk.row_range.start.0, 0);
|
||||
self.unfold_ranges(&[destination..destination], false, false, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.select_ranges(vec![destination..destination]);
|
||||
});
|
||||
}
|
||||
|
||||
hunk
|
||||
) -> Option<MultiBufferRow> {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.diff_hunk_before(position)
|
||||
.or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX))
|
||||
}
|
||||
|
||||
pub fn go_to_definition(
|
||||
@@ -12945,13 +13070,18 @@ impl Editor {
|
||||
}
|
||||
} else {
|
||||
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let buffer_ids: HashSet<_> = multi_buffer_snapshot
|
||||
.ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
|
||||
.map(|(snapshot, _, _)| snapshot.remote_id())
|
||||
let buffer_ids: HashSet<_> = self
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
|
||||
.collect();
|
||||
|
||||
let should_unfold = buffer_ids
|
||||
.iter()
|
||||
.any(|buffer_id| self.is_buffer_folded(*buffer_id, cx));
|
||||
|
||||
for buffer_id in buffer_ids {
|
||||
if self.is_buffer_folded(buffer_id, cx) {
|
||||
if should_unfold {
|
||||
self.unfold_buffer(buffer_id, cx);
|
||||
} else {
|
||||
self.fold_buffer(buffer_id, cx);
|
||||
@@ -13538,20 +13668,20 @@ impl Editor {
|
||||
|
||||
pub fn stage_and_next(
|
||||
&mut self,
|
||||
action: &::git::StageAndNext,
|
||||
_: &::git::StageAndNext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.do_stage_or_unstage_and_next(true, action.whole_excerpt, window, cx);
|
||||
self.do_stage_or_unstage_and_next(true, window, cx);
|
||||
}
|
||||
|
||||
pub fn unstage_and_next(
|
||||
&mut self,
|
||||
action: &::git::UnstageAndNext,
|
||||
_: &::git::UnstageAndNext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.do_stage_or_unstage_and_next(false, action.whole_excerpt, window, cx);
|
||||
self.do_stage_or_unstage_and_next(false, window, cx);
|
||||
}
|
||||
|
||||
pub fn stage_or_unstage_diff_hunks(
|
||||
@@ -13573,90 +13703,33 @@ impl Editor {
|
||||
fn do_stage_or_unstage_and_next(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
whole_excerpt: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
|
||||
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
|
||||
|
||||
if ranges.iter().any(|range| range.start != range.end) {
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if !whole_excerpt {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let newest_range = self.selections.newest::<Point>(cx).range();
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let newest_range = self.selections.newest::<Point>(cx).range();
|
||||
|
||||
let run_twice = snapshot
|
||||
.hunks_for_ranges([newest_range])
|
||||
.first()
|
||||
.is_some_and(|hunk| {
|
||||
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
|
||||
self.hunk_after_position(&snapshot, next_line)
|
||||
.is_some_and(|other| other.row_range == hunk.row_range)
|
||||
});
|
||||
let run_twice = snapshot
|
||||
.hunks_for_ranges([newest_range])
|
||||
.first()
|
||||
.is_some_and(|hunk| {
|
||||
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
|
||||
self.hunk_after_position(&snapshot, next_line)
|
||||
.is_some_and(|other| other.row_range == hunk.row_range)
|
||||
});
|
||||
|
||||
if run_twice {
|
||||
self.go_to_next_hunk(&Default::default(), window, cx);
|
||||
}
|
||||
} else if !self.buffer().read(cx).is_singleton() {
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
|
||||
if let Some((excerpt_id, buffer, range)) = self.active_excerpt(cx) {
|
||||
if buffer.read(cx).is_empty() {
|
||||
let buffer = buffer.read(cx);
|
||||
let Some(file) = buffer.file() else {
|
||||
return;
|
||||
};
|
||||
let project_path = project::ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
};
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(repo) = project.read(cx).git_store().read(cx).active_repository()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
repo.update(cx, |repo, cx| {
|
||||
let Some(repo_path) = repo.project_path_to_repo_path(&project_path) else {
|
||||
return;
|
||||
};
|
||||
let Some(status) = repo.repository_entry.status_for_path(&repo_path) else {
|
||||
return;
|
||||
};
|
||||
if stage && status.status == FileStatus::Untracked {
|
||||
repo.stage_entries(vec![repo_path], cx)
|
||||
.detach_and_log_err(cx);
|
||||
return;
|
||||
}
|
||||
})
|
||||
}
|
||||
ranges = vec![multi_buffer::Anchor::range_in_buffer(
|
||||
excerpt_id,
|
||||
buffer.read(cx).remote_id(),
|
||||
range,
|
||||
)];
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let mut point = ranges.last().unwrap().end.to_point(&snapshot);
|
||||
if point.row < snapshot.max_row().0 {
|
||||
point.row += 1;
|
||||
point.column = 0;
|
||||
point = snapshot.clip_point(point, Bias::Right);
|
||||
self.change_selections(Some(Autoscroll::top_relative(6)), window, cx, |s| {
|
||||
s.select_ranges([point..point]);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if run_twice {
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
self.go_to_next_hunk(&Default::default(), window, cx);
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
|
||||
fn do_stage_or_unstage(
|
||||
@@ -13696,7 +13769,7 @@ impl Editor {
|
||||
buffer_range: hunk.buffer_range,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range,
|
||||
secondary_status: hunk.secondary_status,
|
||||
row_range: 0..0, // unused
|
||||
range: Point::zero()..Point::zero(), // unused
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&buffer_snapshot,
|
||||
@@ -13970,12 +14043,16 @@ impl Editor {
|
||||
return wrap_guides;
|
||||
}
|
||||
|
||||
let settings = self.buffer.read(cx).settings_at(0, cx);
|
||||
let settings = self.buffer.read(cx).language_settings(cx);
|
||||
if settings.show_wrap_guides {
|
||||
if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
|
||||
wrap_guides.push((soft_wrap as usize, true));
|
||||
} else if let SoftWrap::Bounded(soft_wrap) = self.soft_wrap_mode(cx) {
|
||||
wrap_guides.push((soft_wrap as usize, true));
|
||||
match self.soft_wrap_mode(cx) {
|
||||
SoftWrap::Column(soft_wrap) => {
|
||||
wrap_guides.push((soft_wrap as usize, true));
|
||||
}
|
||||
SoftWrap::Bounded(soft_wrap) => {
|
||||
wrap_guides.push((soft_wrap as usize, true));
|
||||
}
|
||||
SoftWrap::GitDiff | SoftWrap::None | SoftWrap::EditorWidth => {}
|
||||
}
|
||||
wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
|
||||
}
|
||||
@@ -13984,7 +14061,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap {
|
||||
let settings = self.buffer.read(cx).settings_at(0, cx);
|
||||
let settings = self.buffer.read(cx).language_settings(cx);
|
||||
let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap);
|
||||
match mode {
|
||||
language_settings::SoftWrap::PreferLine | language_settings::SoftWrap::None => {
|
||||
@@ -14083,7 +14160,7 @@ impl Editor {
|
||||
let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.language_settings(cx)
|
||||
.indent_guides
|
||||
.enabled
|
||||
});
|
||||
@@ -15732,7 +15809,7 @@ impl Editor {
|
||||
let copilot_enabled_for_language = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.language_settings(cx)
|
||||
.show_edit_predictions;
|
||||
|
||||
let project = project.read(cx);
|
||||
@@ -16004,9 +16081,9 @@ impl Editor {
|
||||
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
changes.into_iter().map(|(range, text)| {
|
||||
(range, text.to_string().map(Arc::<str>::from))
|
||||
}),
|
||||
changes
|
||||
.into_iter()
|
||||
.map(|(range, text)| (range, text.to_string())),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
@@ -17124,17 +17201,14 @@ impl EditorSnapshot {
|
||||
for hunk in self.buffer_snapshot.diff_hunks_in_range(
|
||||
Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
|
||||
) {
|
||||
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
|
||||
// when the caret is just above or just below the deleted hunk.
|
||||
let allow_adjacent = hunk.status().is_deleted();
|
||||
let related_to_selection = if allow_adjacent {
|
||||
hunk.row_range.overlaps(&query_rows)
|
||||
|| hunk.row_range.start == query_rows.end
|
||||
|| hunk.row_range.end == query_rows.start
|
||||
} else {
|
||||
hunk.row_range.overlaps(&query_rows)
|
||||
};
|
||||
if related_to_selection {
|
||||
// Include deleted hunks that are adjacent to the query range, because
|
||||
// otherwise they would be missed.
|
||||
let mut intersects_range = hunk.row_range.overlaps(&query_rows);
|
||||
if hunk.status().is_deleted() {
|
||||
intersects_range |= hunk.row_range.start == query_rows.end;
|
||||
intersects_range |= hunk.row_range.end == query_rows.start;
|
||||
}
|
||||
if intersects_range {
|
||||
if !processed_buffer_rows
|
||||
.entry(hunk.buffer_id)
|
||||
.or_default()
|
||||
@@ -17975,7 +18049,7 @@ pub fn diagnostic_block_renderer(
|
||||
)
|
||||
.child(buttons(&diagnostic))
|
||||
.child(div().flex().flex_shrink_0().child(
|
||||
StyledText::new(text_without_backticks.clone()).with_highlights(
|
||||
StyledText::new(text_without_backticks.clone()).with_default_highlights(
|
||||
&text_style,
|
||||
code_ranges.iter().map(|range| {
|
||||
(
|
||||
@@ -18294,3 +18368,37 @@ fn all_edits_insertions_or_deletions(
|
||||
}
|
||||
all_insertions || all_deletions
|
||||
}
|
||||
|
||||
struct MissingEditPredictionKeybindingTooltip;
|
||||
|
||||
impl Render for MissingEditPredictionKeybindingTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
ui::tooltip_container(window, cx, |container, _, cx| {
|
||||
container
|
||||
.flex_shrink_0()
|
||||
.max_w_80()
|
||||
.min_h(rems_from_px(124.))
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.text_ui_sm(cx)
|
||||
.child(Label::new("Conflict with Accept Keybinding"))
|
||||
.child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.")
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1()
|
||||
.gap_1()
|
||||
.items_end()
|
||||
.w_full()
|
||||
.child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx)
|
||||
}))
|
||||
.child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
|
||||
cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding");
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1514,6 +1514,10 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
|
||||
stop_at_indent: true,
|
||||
};
|
||||
|
||||
let delete_to_beg = DeleteToBeginningOfLine {
|
||||
stop_at_indent: false,
|
||||
};
|
||||
|
||||
let move_to_end = MoveToEndOfLine {
|
||||
stop_at_soft_wraps: true,
|
||||
};
|
||||
@@ -1672,7 +1676,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
|
||||
editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
|
||||
assert_eq!(editor.display_text(cx), "\n");
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
@@ -1778,6 +1782,107 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let move_to_beg = MoveToBeginningOfLine {
|
||||
stop_at_soft_wraps: true,
|
||||
stop_at_indent: true,
|
||||
};
|
||||
|
||||
let select_to_beg = SelectToBeginningOfLine {
|
||||
stop_at_soft_wraps: true,
|
||||
stop_at_indent: true,
|
||||
};
|
||||
|
||||
let delete_to_beg = DeleteToBeginningOfLine {
|
||||
stop_at_indent: true,
|
||||
};
|
||||
|
||||
let move_to_end = MoveToEndOfLine {
|
||||
stop_at_soft_wraps: false,
|
||||
};
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("abc\n def", cx);
|
||||
build_editor(buffer, window, cx)
|
||||
});
|
||||
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
|
||||
DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
|
||||
]);
|
||||
});
|
||||
|
||||
// Moving to the beginning of the line should put the first cursor at the beginning of the line,
|
||||
// and the second cursor at the first non-whitespace character in the line.
|
||||
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
|
||||
]
|
||||
);
|
||||
|
||||
// Moving to the beginning of the line again should be a no-op for the first cursor,
|
||||
// and should move the second cursor to the beginning of the line.
|
||||
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
|
||||
]
|
||||
);
|
||||
|
||||
// Moving to the beginning of the line again should still be a no-op for the first cursor,
|
||||
// and should move the second cursor back to the first non-whitespace character in the line.
|
||||
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
|
||||
]
|
||||
);
|
||||
|
||||
// Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
|
||||
// and to the first non-whitespace character in the line for the second cursor.
|
||||
editor.move_to_end_of_line(&move_to_end, window, cx);
|
||||
editor.move_left(&MoveLeft, window, cx);
|
||||
editor.select_to_beginning_of_line(&select_to_beg, window, cx);
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
|
||||
]
|
||||
);
|
||||
|
||||
// Selecting to the beginning of the line again should be a no-op for the first cursor,
|
||||
// and should select to the beginning of the line for the second cursor.
|
||||
editor.select_to_beginning_of_line(&select_to_beg, window, cx);
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
|
||||
]
|
||||
);
|
||||
|
||||
// Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
|
||||
// and should delete to the first non-whitespace character in the line for the second cursor.
|
||||
editor.move_to_end_of_line(&move_to_end, window, cx);
|
||||
editor.move_left(&MoveLeft, window, cx);
|
||||
editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
|
||||
assert_eq!(editor.text(cx), "c\n f");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -2295,7 +2400,13 @@ async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("one «two threeˇ» four");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
|
||||
editor.delete_to_beginning_of_line(
|
||||
&DeleteToBeginningOfLine {
|
||||
stop_at_indent: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(editor.text(cx), " four");
|
||||
});
|
||||
}
|
||||
@@ -2854,7 +2965,7 @@ async fn test_indent_outdent(cx: &mut TestAppContext) {
|
||||
four
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«oneˇ» «twoˇ»
|
||||
three
|
||||
@@ -2874,7 +2985,7 @@ async fn test_indent_outdent(cx: &mut TestAppContext) {
|
||||
ˇ» four
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one two
|
||||
t«hree
|
||||
@@ -2899,7 +3010,7 @@ async fn test_indent_outdent(cx: &mut TestAppContext) {
|
||||
ˇ three
|
||||
four
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one two
|
||||
ˇthree
|
||||
@@ -2933,13 +3044,13 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
|
||||
three
|
||||
four
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
\t«oneˇ» «twoˇ»
|
||||
three
|
||||
four
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«oneˇ» «twoˇ»
|
||||
three
|
||||
@@ -2964,13 +3075,13 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
|
||||
\t\tt«hree
|
||||
ˇ»four
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one two
|
||||
\tt«hree
|
||||
ˇ»four
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one two
|
||||
t«hree
|
||||
@@ -2983,7 +3094,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
|
||||
ˇthree
|
||||
four
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one two
|
||||
ˇthree
|
||||
@@ -2995,7 +3106,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
|
||||
\tˇthree
|
||||
four
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
|
||||
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one two
|
||||
ˇthree
|
||||
@@ -3100,7 +3211,7 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
editor.tab_prev(&TabPrev, window, cx);
|
||||
editor.backtab(&Backtab, window, cx);
|
||||
assert_text_with_selections(
|
||||
&mut editor,
|
||||
indoc! {"
|
||||
@@ -10915,7 +11026,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -10924,7 +11035,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -10933,7 +11044,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -10942,7 +11053,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -11022,7 +11133,7 @@ async fn cycle_through_same_place_diagnostics(
|
||||
|
||||
// Fourth diagnostic
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn func(abc def: i32) -> ˇu32 {
|
||||
@@ -11031,7 +11142,7 @@ async fn cycle_through_same_place_diagnostics(
|
||||
|
||||
// Third diagnostic
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn func(abc ˇdef: i32) -> u32 {
|
||||
@@ -11040,7 +11151,7 @@ async fn cycle_through_same_place_diagnostics(
|
||||
|
||||
// Second diagnostic, same place
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn func(abc ˇdef: i32) -> u32 {
|
||||
@@ -11049,7 +11160,7 @@ async fn cycle_through_same_place_diagnostics(
|
||||
|
||||
// First diagnostic
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn func(abcˇ def: i32) -> u32 {
|
||||
@@ -11058,7 +11169,7 @@ async fn cycle_through_same_place_diagnostics(
|
||||
|
||||
// Wrapped over, fourth diagnostic
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
|
||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn func(abc def: i32) -> ˇu32 {
|
||||
@@ -11324,7 +11435,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
//Wrap around the top of the buffer
|
||||
for _ in 0..2 {
|
||||
editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11344,7 +11455,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(
|
||||
@@ -11363,7 +11474,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(
|
||||
@@ -11383,7 +11494,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
for _ in 0..2 {
|
||||
editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12118,7 +12229,7 @@ async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.context_menu_prev(&ContextMenuPrev, window, cx);
|
||||
editor.context_menu_prev(&ContextMenuPrevious, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -12308,7 +12419,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||
resolved_items.lock().clear();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.context_menu_prev(&ContextMenuPrev, window, cx);
|
||||
editor.context_menu_prev(&ContextMenuPrevious, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
// Completions that have already been resolved are skipped.
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint,
|
||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
|
||||
GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
||||
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown,
|
||||
PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
|
||||
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
||||
@@ -32,15 +32,17 @@ use collections::{BTreeMap, HashMap, HashSet};
|
||||
use file_icons::FileIcons;
|
||||
use git::{blame::BlameEntry, status::FileStatus, Oid};
|
||||
use gpui::{
|
||||
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
|
||||
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
|
||||
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
|
||||
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
||||
Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
Subscription, TextRun, TextStyleRefinement, Window,
|
||||
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
|
||||
point, px, quad, relative, size, solid_background, svg, transparent_black, Action, AnyElement,
|
||||
App, AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner,
|
||||
Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement,
|
||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
|
||||
SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun,
|
||||
TextStyleRefinement, Window,
|
||||
};
|
||||
use inline_completion::Direction;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{
|
||||
@@ -54,7 +56,7 @@ use multi_buffer::{
|
||||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
RowInfo,
|
||||
};
|
||||
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
|
||||
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
|
||||
use settings::Settings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
@@ -195,7 +197,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::backspace);
|
||||
register_action(editor, window, Editor::delete);
|
||||
register_action(editor, window, Editor::tab);
|
||||
register_action(editor, window, Editor::tab_prev);
|
||||
register_action(editor, window, Editor::backtab);
|
||||
register_action(editor, window, Editor::indent);
|
||||
register_action(editor, window, Editor::outdent);
|
||||
register_action(editor, window, Editor::autoindent);
|
||||
@@ -2016,7 +2018,7 @@ impl EditorElement {
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -2092,7 +2094,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
display_hunks,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -2110,7 +2112,7 @@ impl EditorElement {
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
@@ -2135,7 +2137,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
display_hunks,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -2722,7 +2724,10 @@ impl EditorElement {
|
||||
.shadow_md()
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected && is_folded {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
@@ -4343,7 +4348,7 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
|
||||
if layout.display_hunks.is_empty() {
|
||||
@@ -4413,10 +4418,19 @@ impl EditorElement {
|
||||
background_color =
|
||||
background_color.opacity(if is_light { 0.2 } else { 0.32 });
|
||||
}
|
||||
|
||||
// Flatten the background color with the editor color to prevent
|
||||
// elements below transparent hunks from showing through
|
||||
let flattened_background_color = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.blend(background_color);
|
||||
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
background_color,
|
||||
flattened_background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
@@ -4544,7 +4558,7 @@ impl EditorElement {
|
||||
)
|
||||
});
|
||||
if show_git_gutter {
|
||||
Self::paint_diff_hunks(layout, window, cx)
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
|
||||
let highlight_width = 0.275 * layout.position_map.line_height;
|
||||
@@ -4682,7 +4696,7 @@ impl EditorElement {
|
||||
.read(cx)
|
||||
.buffer
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.language_settings(cx)
|
||||
.show_whitespaces;
|
||||
|
||||
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
|
||||
@@ -5675,7 +5689,7 @@ fn prepaint_gutter_button(
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
@@ -5687,9 +5701,23 @@ fn prepaint_gutter_button(
|
||||
let indicator_size = button.layout_as_root(available_space, window, cx);
|
||||
|
||||
let blame_width = gutter_dimensions.git_blame_entries_width;
|
||||
let gutter_width = rows_with_hunk_bounds
|
||||
.get(&row)
|
||||
.map(|bounds| bounds.size.width);
|
||||
let gutter_width = display_hunks
|
||||
.binary_search_by(|(hunk, _)| match hunk {
|
||||
DisplayDiffHunk::Folded { display_row } => display_row.cmp(&row),
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => {
|
||||
if display_row_range.end <= row {
|
||||
Ordering::Less
|
||||
} else if display_row_range.start > row {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.and_then(|ix| Some(display_hunks[ix].1.as_ref()?.size.width));
|
||||
let left_offset = blame_width.max(gutter_width).unwrap_or_default();
|
||||
|
||||
let mut x = left_offset;
|
||||
@@ -6708,15 +6736,16 @@ impl Element for EditorElement {
|
||||
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
|
||||
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
let use_pattern = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.hunk_style
|
||||
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
|
||||
|
||||
for (ix, row_info) in row_infos.iter().enumerate() {
|
||||
let Some(diff_status) = row_info.diff_status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let staged_opacity = if is_light { 0.14 } else { 0.10 };
|
||||
let unstaged_opacity = 0.04;
|
||||
|
||||
let background_color = match diff_status.kind {
|
||||
DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
|
||||
DiffHunkStatusKind::Deleted => {
|
||||
@@ -6727,15 +6756,34 @@ impl Element for EditorElement {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let background_color = if diff_status.has_secondary_hunk() {
|
||||
background_color.opacity(unstaged_opacity)
|
||||
|
||||
let unstaged = diff_status.has_secondary_hunk();
|
||||
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
|
||||
|
||||
let staged_background =
|
||||
solid_background(background_color.opacity(hunk_opacity));
|
||||
let unstaged_background = if use_pattern {
|
||||
pattern_slash(
|
||||
background_color.opacity(hunk_opacity),
|
||||
window.rem_size().0 * 1.125, // ~18 by default
|
||||
)
|
||||
} else {
|
||||
background_color.opacity(staged_opacity)
|
||||
solid_background(background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
0.04
|
||||
}))
|
||||
};
|
||||
|
||||
let background = if unstaged {
|
||||
unstaged_background
|
||||
} else {
|
||||
staged_background
|
||||
};
|
||||
|
||||
highlighted_rows
|
||||
.entry(start_row + DisplayRow(ix as u32))
|
||||
.or_insert(background_color.into());
|
||||
.or_insert(background);
|
||||
}
|
||||
|
||||
let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
|
||||
@@ -7185,27 +7233,6 @@ impl Element for EditorElement {
|
||||
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
let rows_with_hunk_bounds = display_hunks
|
||||
.iter()
|
||||
.filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds)))
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
|mut rows_with_hunk_bounds, (hunk, bounds)| {
|
||||
match hunk {
|
||||
DisplayDiffHunk::Folded { display_row } => {
|
||||
rows_with_hunk_bounds.insert(*display_row, bounds);
|
||||
}
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => {
|
||||
for display_row in display_row_range.iter_rows() {
|
||||
rows_with_hunk_bounds.insert(display_row, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
rows_with_hunk_bounds
|
||||
},
|
||||
);
|
||||
let mut code_actions_indicator = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let newest_selection_point =
|
||||
@@ -7255,7 +7282,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&display_hunks,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -7283,7 +7310,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&display_hunks,
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -8887,8 +8914,13 @@ fn diff_hunk_controls(
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let position =
|
||||
hunk_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
editor
|
||||
.go_to_hunk_after_position(&snapshot, position, window, cx);
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
position,
|
||||
Direction::Next,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.expand_selected_diff_hunks(cx);
|
||||
});
|
||||
}
|
||||
@@ -8904,7 +8936,7 @@ fn diff_hunk_controls(
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Previous Hunk",
|
||||
&GoToPrevHunk,
|
||||
&GoToPreviousHunk,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -8918,7 +8950,13 @@ fn diff_hunk_controls(
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let point =
|
||||
hunk_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_before_position(&snapshot, point, window, cx);
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
point,
|
||||
Direction::Prev,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.expand_selected_diff_hunks(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -618,12 +618,12 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
|
||||
heading: StyleRefinement::default()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.text_base()
|
||||
.mt(rems(1.))
|
||||
.mb_0(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -581,6 +581,7 @@ impl InlayHintCache {
|
||||
self.version += 1;
|
||||
}
|
||||
self.update_tasks.clear();
|
||||
self.refresh_task = Task::ready(());
|
||||
self.hints.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -618,11 +618,8 @@ impl Item for Editor {
|
||||
ItemSettings::get_global(cx)
|
||||
.file_icons
|
||||
.then(|| {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
||||
.and_then(|path| FileIcons::get_icon(path.path.as_ref(), cx))
|
||||
path_for_buffer(&self.buffer, 0, true, cx)
|
||||
.and_then(|path| FileIcons::get_icon(path.as_ref(), cx))
|
||||
})
|
||||
.flatten()
|
||||
.map(Icon::from_path)
|
||||
@@ -1592,11 +1589,13 @@ impl SearchableItem for Editor {
|
||||
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
matches: &[Range<Anchor>],
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
active_match_index(
|
||||
direction,
|
||||
matches,
|
||||
&self.selections.newest_anchor().head(),
|
||||
&self.buffer().read(cx).snapshot(cx),
|
||||
@@ -1609,6 +1608,7 @@ impl SearchableItem for Editor {
|
||||
}
|
||||
|
||||
pub fn active_match_index(
|
||||
direction: Direction,
|
||||
ranges: &[Range<Anchor>],
|
||||
cursor: &Anchor,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
@@ -1616,7 +1616,7 @@ pub fn active_match_index(
|
||||
if ranges.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match ranges.binary_search_by(|probe| {
|
||||
let r = ranges.binary_search_by(|probe| {
|
||||
if probe.end.cmp(cursor, buffer).is_lt() {
|
||||
Ordering::Less
|
||||
} else if probe.start.cmp(cursor, buffer).is_gt() {
|
||||
@@ -1624,8 +1624,15 @@ pub fn active_match_index(
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
|
||||
});
|
||||
match direction {
|
||||
Direction::Prev => match r {
|
||||
Ok(i) => Some(i),
|
||||
Err(i) => Some(i.saturating_sub(1)),
|
||||
},
|
||||
Direction::Next => match r {
|
||||
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ impl SignatureHelpPopover {
|
||||
.child(
|
||||
div().px_4().pb_1().child(
|
||||
StyledText::new(self.label.clone())
|
||||
.with_highlights(&self.style, self.highlights.iter().cloned()),
|
||||
.with_default_highlights(&self.style, self.highlights.iter().cloned()),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
|
||||
@@ -12,7 +12,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use multi_buffer::{ExcerptRange, MultiBufferRow};
|
||||
use multi_buffer::{Anchor, ExcerptRange, MultiBufferRow};
|
||||
use parking_lot::RwLock;
|
||||
use project::{FakeFs, Project};
|
||||
use std::{
|
||||
@@ -89,6 +89,16 @@ impl EditorTestContext {
|
||||
Path::new("/root")
|
||||
}
|
||||
|
||||
pub async fn for_editor_in(editor: Entity<Editor>, cx: &mut gpui::VisualTestContext) -> Self {
|
||||
cx.focus(&editor);
|
||||
Self {
|
||||
window: cx.windows()[0],
|
||||
cx: cx.clone(),
|
||||
editor,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
|
||||
let editor_view = editor.root(cx).unwrap();
|
||||
Self {
|
||||
@@ -381,6 +391,85 @@ impl EditorTestContext {
|
||||
assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
|
||||
let expected_excerpts = marked_text
|
||||
.strip_prefix("[EXCERPT]\n")
|
||||
.unwrap()
|
||||
.split("[EXCERPT]\n")
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
|
||||
let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
|
||||
|
||||
let selections = editor.selections.disjoint_anchors();
|
||||
let excerpts = multibuffer_snapshot
|
||||
.excerpts()
|
||||
.map(|(e_id, snapshot, range)| (e_id, snapshot.clone(), range))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(multibuffer_snapshot, selections, excerpts)
|
||||
});
|
||||
|
||||
assert!(
|
||||
excerpts.len() == expected_excerpts.len(),
|
||||
"should have {} excerpts, got {}",
|
||||
expected_excerpts.len(),
|
||||
excerpts.len()
|
||||
);
|
||||
|
||||
for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
|
||||
let is_folded = self
|
||||
.update_editor(|editor, _, cx| editor.is_buffer_folded(snapshot.remote_id(), cx));
|
||||
let (expected_text, expected_selections) =
|
||||
marked_text_ranges(expected_excerpts[ix], true);
|
||||
if expected_text == "[FOLDED]\n" {
|
||||
assert!(is_folded, "excerpt {} should be folded", ix);
|
||||
let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id);
|
||||
if expected_selections.len() > 0 {
|
||||
assert!(
|
||||
is_selected,
|
||||
"excerpt {} should be selected. Got {:?}",
|
||||
ix,
|
||||
self.editor_state()
|
||||
);
|
||||
} else {
|
||||
assert!(!is_selected, "excerpt {} should not be selected", ix);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
assert!(!is_folded, "excerpt {} should not be folded", ix);
|
||||
assert_eq!(
|
||||
multibuffer_snapshot
|
||||
.text_for_range(Anchor::range_in_buffer(
|
||||
excerpt_id,
|
||||
snapshot.remote_id(),
|
||||
range.context.clone()
|
||||
))
|
||||
.collect::<String>(),
|
||||
expected_text
|
||||
);
|
||||
|
||||
let selections = selections
|
||||
.iter()
|
||||
.filter(|s| s.head().excerpt_id == excerpt_id)
|
||||
.map(|s| {
|
||||
let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
|
||||
- text::ToOffset::to_offset(&range.context.start, &snapshot);
|
||||
let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
|
||||
- text::ToOffset::to_offset(&range.context.start, &snapshot);
|
||||
tail..head
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// todo: selections that cross excerpt boundaries..
|
||||
assert_eq!(
|
||||
selections, expected_selections,
|
||||
"excerpt {} has incorrect selections",
|
||||
ix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Make an assertion about the editor's text and the ranges and directions
|
||||
/// of its selections using a string containing embedded range markers.
|
||||
///
|
||||
@@ -392,6 +481,17 @@ impl EditorTestContext {
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
/// Make an assertion about the editor's text and the ranges and directions
|
||||
/// of its selections using a string containing embedded range markers.
|
||||
///
|
||||
/// See the `util::test::marked_text_ranges` function for more information.
|
||||
#[track_caller]
|
||||
pub fn assert_display_state(&mut self, marked_text: &str) {
|
||||
let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
|
||||
pretty_assertions::assert_eq!(self.display_text(), expected_text, "unexpected buffer text");
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
pub fn editor_state(&mut self) -> String {
|
||||
generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
|
||||
}
|
||||
|
||||
@@ -339,6 +339,20 @@ async fn test_themes(
|
||||
let theme_path = extension_path.join(relative_theme_path);
|
||||
let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?;
|
||||
log::info!("loaded theme family {}", theme_family.name);
|
||||
|
||||
for theme in &theme_family.themes {
|
||||
if theme
|
||||
.style
|
||||
.colors
|
||||
.deprecated_scrollbar_thumb_background
|
||||
.is_some()
|
||||
{
|
||||
bail!(
|
||||
r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
|
||||
theme_name = theme.name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -168,11 +168,14 @@ pub(crate) fn suggest(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Cont
|
||||
);
|
||||
|
||||
workspace.show_notification(notification_id, cx, |cx| {
|
||||
cx.new(move |_cx| {
|
||||
MessageNotification::new(format!(
|
||||
"Do you want to install the recommended '{}' extension for '{}' files?",
|
||||
extension_id, file_name_or_extension
|
||||
))
|
||||
cx.new(move |cx| {
|
||||
MessageNotification::new(
|
||||
format!(
|
||||
"Do you want to install the recommended '{}' extension for '{}' files?",
|
||||
extension_id, file_name_or_extension
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.primary_message("Yes, install extension")
|
||||
.primary_icon(IconName::Check)
|
||||
.primary_icon_color(Color::Success)
|
||||
|
||||
@@ -522,7 +522,7 @@ impl ExtensionsPage {
|
||||
extension.authors.join(", ")
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.text_ellipsis(),
|
||||
.truncate(),
|
||||
)
|
||||
.child(Label::new("<>").size(LabelSize::Small)),
|
||||
)
|
||||
@@ -534,7 +534,7 @@ impl ExtensionsPage {
|
||||
Label::new(description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.text_ellipsis()
|
||||
.truncate()
|
||||
}))
|
||||
.children(repository_url.map(|repository_url| {
|
||||
IconButton::new(
|
||||
@@ -665,7 +665,7 @@ impl ExtensionsPage {
|
||||
extension.manifest.authors.join(", ")
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.text_ellipsis(),
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(format!(
|
||||
@@ -683,7 +683,7 @@ impl ExtensionsPage {
|
||||
Label::new(description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.text_ellipsis()
|
||||
.truncate()
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod file_finder_tests;
|
||||
#[cfg(test)]
|
||||
mod open_path_prompt_tests;
|
||||
|
||||
pub mod file_finder_settings;
|
||||
mod new_path_prompt;
|
||||
@@ -44,7 +46,7 @@ use workspace::{
|
||||
Workspace,
|
||||
};
|
||||
|
||||
actions!(file_finder, [SelectPrev, ToggleMenu]);
|
||||
actions!(file_finder, [SelectPrevious, ToggleMenu]);
|
||||
|
||||
impl ModalView for FileFinder {
|
||||
fn on_before_dismiss(
|
||||
@@ -199,9 +201,14 @@ impl FileFinder {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_select_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn handle_select_prev(
|
||||
&mut self,
|
||||
_: &SelectPrevious,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.init_modifiers = Some(window.modifiers());
|
||||
window.dispatch_action(Box::new(menu::SelectPrev), cx);
|
||||
window.dispatch_action(Box::new(menu::SelectPrevious), cx);
|
||||
}
|
||||
|
||||
fn handle_toggle_menu(&mut self, _: &ToggleMenu, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{assert_eq, future::IntoFuture, path::Path, time::Duration};
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use gpui::{Entity, TestAppContext, VisualTestContext};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use menu::{Confirm, SelectNext, SelectPrevious};
|
||||
use project::{RemoveOptions, FS_WATCH_LATENCY};
|
||||
use serde_json::json;
|
||||
use util::path;
|
||||
@@ -2059,7 +2059,7 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav(
|
||||
// Switch to navigating with other shortcuts
|
||||
// Don't open file on modifiers release
|
||||
cx.simulate_modifiers_change(Modifiers::control());
|
||||
cx.dispatch_action(menu::SelectPrev);
|
||||
cx.dispatch_action(menu::SelectPrevious);
|
||||
cx.simulate_modifiers_change(Modifiers::none());
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
@@ -2071,7 +2071,7 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav(
|
||||
// Back to navigation with initial shortcut
|
||||
// Open file on modifiers release
|
||||
cx.simulate_modifiers_change(Modifiers::secondary_key());
|
||||
cx.dispatch_action(SelectPrev); // <-- File Finder's SelectPrev, not menu's
|
||||
cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
|
||||
cx.simulate_modifiers_change(Modifiers::none());
|
||||
cx.read(|cx| {
|
||||
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
|
||||
|
||||
@@ -192,7 +192,7 @@ impl Match {
|
||||
}
|
||||
}
|
||||
|
||||
StyledText::new(text).with_highlights(&window.text_style().clone(), highlights)
|
||||
StyledText::new(text).with_default_highlights(&window.text_style().clone(), highlights)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use fuzzy::StringMatchCandidate;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::DirectoryLister;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR_STR},
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc,
|
||||
@@ -38,14 +38,38 @@ impl OpenPathDelegate {
|
||||
should_dismiss: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn collect_match_candidates(&self) -> Vec<String> {
|
||||
if let Some(state) = self.directory_state.as_ref() {
|
||||
self.matches
|
||||
.iter()
|
||||
.filter_map(|&index| {
|
||||
state
|
||||
.match_candidates
|
||||
.get(index)
|
||||
.map(|candidate| candidate.path.string.clone())
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DirectoryState {
|
||||
path: String,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
match_candidates: Vec<CandidateInfo>,
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CandidateInfo {
|
||||
path: StringMatchCandidate,
|
||||
is_dir: bool,
|
||||
}
|
||||
|
||||
impl OpenPathPrompt {
|
||||
pub(crate) fn register(
|
||||
workspace: &mut Workspace,
|
||||
@@ -93,8 +117,6 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
// Is this method woring correctly on Windows? This method uses `/` for path separator.
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
@@ -102,13 +124,26 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let lister = self.lister.clone();
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
let query_path = Path::new(&query);
|
||||
let last_item = query_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(&last_item) {
|
||||
(dir.to_string(), last_item)
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if dir == "" {
|
||||
dir = "/".to_string();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
dir = "/".to_string();
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
dir = "C:\\".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let query = if self
|
||||
@@ -134,12 +169,16 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.delegate.directory_state = Some(match paths {
|
||||
Ok(mut paths) => {
|
||||
paths.sort_by(|a, b| compare_paths((a, true), (b, true)));
|
||||
paths.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
|
||||
let match_candidates = paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, path)| {
|
||||
StringMatchCandidate::new(ix, &path.to_string_lossy())
|
||||
.map(|(ix, item)| CandidateInfo {
|
||||
path: StringMatchCandidate::new(
|
||||
ix,
|
||||
&item.path.to_string_lossy(),
|
||||
),
|
||||
is_dir: item.is_dir,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -178,7 +217,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
};
|
||||
|
||||
if !suffix.starts_with('.') {
|
||||
match_candidates.retain(|m| !m.string.starts_with('.'));
|
||||
match_candidates.retain(|m| !m.path.string.starts_with('.'));
|
||||
}
|
||||
|
||||
if suffix == "" {
|
||||
@@ -186,7 +225,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate
|
||||
.matches
|
||||
.extend(match_candidates.iter().map(|m| m.id));
|
||||
.extend(match_candidates.iter().map(|m| m.path.id));
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
@@ -194,8 +233,9 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
let candidates = match_candidates.iter().map(|m| &m.path).collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
match_candidates.as_slice(),
|
||||
candidates.as_slice(),
|
||||
&suffix,
|
||||
false,
|
||||
100,
|
||||
@@ -217,7 +257,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
this.delegate.directory_state.as_ref().and_then(|d| {
|
||||
d.match_candidates
|
||||
.get(*m)
|
||||
.map(|c| !c.string.starts_with(&suffix))
|
||||
.map(|c| !c.path.string.starts_with(&suffix))
|
||||
}),
|
||||
*m,
|
||||
)
|
||||
@@ -239,7 +279,16 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
let m = self.matches.get(self.selected_index)?;
|
||||
let directory_state = self.directory_state.as_ref()?;
|
||||
let candidate = directory_state.match_candidates.get(*m)?;
|
||||
Some(format!("{}/{}", directory_state.path, candidate.string))
|
||||
Some(format!(
|
||||
"{}{}{}",
|
||||
directory_state.path,
|
||||
candidate.path.string,
|
||||
if candidate.is_dir {
|
||||
MAIN_SEPARATOR_STR
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
})
|
||||
.unwrap_or(query),
|
||||
)
|
||||
@@ -260,7 +309,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.resolve_tilde(&directory_state.path, cx)
|
||||
.as_ref(),
|
||||
)
|
||||
.join(&candidate.string);
|
||||
.join(&candidate.path.string);
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(Some(vec![result])).ok();
|
||||
}
|
||||
@@ -294,7 +343,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(LabelLike::new().child(candidate.string.clone())),
|
||||
.child(LabelLike::new().child(candidate.path.string.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -307,6 +356,6 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
Arc::from("[directory/]filename.ext")
|
||||
Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
|
||||
}
|
||||
}
|
||||
|
||||
324
crates/file_finder/src/open_path_prompt_tests.rs
Normal file
324
crates/file_finder/src/open_path_prompt_tests.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, Entity, TestAppContext, VisualTestContext};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use ui::rems;
|
||||
use util::path;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
use crate::OpenPathDelegate;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_path_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a1": "A1",
|
||||
"a2": "A2",
|
||||
"a3": "A3",
|
||||
"dir1": {},
|
||||
"dir2": {
|
||||
"c": "C",
|
||||
"d1": "D1",
|
||||
"d2": "D2",
|
||||
"d3": "D3",
|
||||
"dir3": {},
|
||||
"dir4": {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, cx);
|
||||
|
||||
let query = path!("/root");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
|
||||
|
||||
// If the query ends with a slash, the picker should show the contents of the directory.
|
||||
let query = path!("/root/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a1", "a2", "a3", "dir1", "dir2"]
|
||||
);
|
||||
|
||||
// Show candidates for the query "a".
|
||||
let query = path!("/root/a");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a1", "a2", "a3"]
|
||||
);
|
||||
|
||||
// Show candidates for the query "d".
|
||||
let query = path!("/root/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
|
||||
let query = path!("/root/dir2");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir2"]);
|
||||
|
||||
let query = path!("/root/dir2/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["c", "d1", "d2", "d3", "dir3", "dir4"]
|
||||
);
|
||||
|
||||
// Show candidates for the query "d".
|
||||
let query = path!("/root/dir2/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["d1", "d2", "d3", "dir3", "dir4"]
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/di");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir3", "dir4"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a": "A",
|
||||
"dir1": {},
|
||||
"dir2": {
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"dir3": {},
|
||||
"dir4": {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, cx);
|
||||
|
||||
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
|
||||
let query = path!("/root");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/"));
|
||||
|
||||
// Confirm completion for the query "/root/", selecting the first candidate "a", since it's a file, it should not add a trailing slash.
|
||||
let query = path!("/root/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
|
||||
|
||||
// Confirm completion for the query "/root/", selecting the second candidate "dir1", since it's a directory, it should add a trailing slash.
|
||||
let query = path!("/root/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 1, &picker, cx),
|
||||
path!("/root/dir1/")
|
||||
);
|
||||
|
||||
let query = path!("/root/a");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
|
||||
|
||||
let query = path!("/root/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 1, &picker, cx),
|
||||
path!("/root/dir2/")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 0, &picker, cx),
|
||||
path!("/root/dir2/")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 0, &picker, cx),
|
||||
path!("/root/dir2/c")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 2, &picker, cx),
|
||||
path!("/root/dir2/dir3/")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 0, &picker, cx),
|
||||
path!("/root/dir2/d")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 1, &picker, cx),
|
||||
path!("/root/dir2/dir3/")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/di");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 1, &picker, cx),
|
||||
path!("/root/dir2/dir4/")
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a": "A",
|
||||
"dir1": {},
|
||||
"dir2": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, cx);
|
||||
|
||||
// Support both forward and backward slashes.
|
||||
let query = "C:/root/";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a", "dir1", "dir2"]
|
||||
);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:/root/a");
|
||||
|
||||
let query = "C:\\root/";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a", "dir1", "dir2"]
|
||||
);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/a");
|
||||
|
||||
let query = "C:\\root\\";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a", "dir1", "dir2"]
|
||||
);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root\\a");
|
||||
|
||||
// Confirm completion for the query "C:/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
|
||||
let query = "C:/root/d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(confirm_completion(query, 1, &picker, cx), "C:/root/dir2\\");
|
||||
|
||||
let query = "C:\\root/d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/dir1\\");
|
||||
|
||||
let query = "C:\\root\\d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(
|
||||
confirm_completion(query, 0, &picker, cx),
|
||||
"C:\\root\\dir1\\"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let state = AppState::test(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
state
|
||||
})
|
||||
}
|
||||
|
||||
fn build_open_path_prompt(
|
||||
project: Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
|
||||
let (tx, _) = futures::channel::oneshot::channel();
|
||||
let lister = project::DirectoryLister::Project(project.clone());
|
||||
let delegate = OpenPathDelegate::new(tx, lister.clone());
|
||||
|
||||
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
(
|
||||
workspace.update_in(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, window, cx)
|
||||
.width(rems(34.))
|
||||
.modal(false);
|
||||
let query = lister.default_query(cx);
|
||||
picker.set_query(query, window, cx);
|
||||
picker
|
||||
})
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
async fn insert_query(
|
||||
query: &str,
|
||||
picker: &Entity<Picker<OpenPathDelegate>>,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
picker
|
||||
.update_in(cx, |f, window, cx| {
|
||||
f.delegate.update_matches(query.to_string(), window, cx)
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
fn confirm_completion(
|
||||
query: &str,
|
||||
select: usize,
|
||||
picker: &Entity<Picker<OpenPathDelegate>>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> String {
|
||||
picker
|
||||
.update_in(cx, |f, window, cx| {
|
||||
if f.delegate.selected_index() != select {
|
||||
f.delegate.set_selected_index(select, window, cx);
|
||||
}
|
||||
f.delegate.confirm_completion(query.to_string(), window, cx)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn collect_match_candidates(
|
||||
picker: &Entity<Picker<OpenPathDelegate>>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Vec<String> {
|
||||
picker.update(cx, |f, _| f.delegate.collect_match_candidates())
|
||||
}
|
||||
@@ -135,6 +135,7 @@ pub trait Fs: Send + Sync {
|
||||
Arc<dyn Watcher>,
|
||||
);
|
||||
|
||||
fn home_dir(&self) -> Option<PathBuf>;
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
|
||||
fn is_fake(&self) -> bool;
|
||||
async fn is_case_sensitive(&self) -> Result<bool>;
|
||||
@@ -813,6 +814,10 @@ impl Fs for RealFs {
|
||||
temp_dir.close()?;
|
||||
case_sensitive
|
||||
}
|
||||
|
||||
fn home_dir(&self) -> Option<PathBuf> {
|
||||
Some(paths::home_dir().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
@@ -846,6 +851,7 @@ struct FakeFsState {
|
||||
metadata_call_count: usize,
|
||||
read_dir_call_count: usize,
|
||||
moves: std::collections::HashMap<u64, PathBuf>,
|
||||
home_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -1031,6 +1037,7 @@ impl FakeFs {
|
||||
read_dir_call_count: 0,
|
||||
metadata_call_count: 0,
|
||||
moves: Default::default(),
|
||||
home_dir: None,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1524,6 +1531,10 @@ impl FakeFs {
|
||||
fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
|
||||
self.executor.simulate_random_delay()
|
||||
}
|
||||
|
||||
pub fn set_home_dir(&self, home_dir: PathBuf) {
|
||||
self.state.lock().home_dir = Some(home_dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -2079,6 +2090,10 @@ impl Fs for FakeFs {
|
||||
fn as_fake(&self) -> Arc<FakeFs> {
|
||||
self.this.upgrade().unwrap()
|
||||
}
|
||||
|
||||
fn home_dir(&self) -> Option<PathBuf> {
|
||||
self.state.lock().home_dir.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
borrow::{Borrow, Cow},
|
||||
sync::atomic::{self, AtomicBool},
|
||||
};
|
||||
|
||||
@@ -50,22 +50,24 @@ impl<'a> Matcher<'a> {
|
||||
|
||||
/// Filter and score fuzzy match candidates. Results are returned unsorted, in the same order as
|
||||
/// the input candidates.
|
||||
pub fn match_candidates<C: MatchCandidate, R, F>(
|
||||
pub fn match_candidates<C, R, F, T>(
|
||||
&mut self,
|
||||
prefix: &[char],
|
||||
lowercase_prefix: &[char],
|
||||
candidates: impl Iterator<Item = C>,
|
||||
candidates: impl Iterator<Item = T>,
|
||||
results: &mut Vec<R>,
|
||||
cancel_flag: &AtomicBool,
|
||||
build_match: F,
|
||||
) where
|
||||
C: MatchCandidate,
|
||||
T: Borrow<C>,
|
||||
F: Fn(&C, f64, &Vec<usize>) -> R,
|
||||
{
|
||||
let mut candidate_chars = Vec::new();
|
||||
let mut lowercase_candidate_chars = Vec::new();
|
||||
|
||||
for candidate in candidates {
|
||||
if !candidate.has_chars(self.query_char_bag) {
|
||||
if !candidate.borrow().has_chars(self.query_char_bag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ impl<'a> Matcher<'a> {
|
||||
|
||||
candidate_chars.clear();
|
||||
lowercase_candidate_chars.clear();
|
||||
for c in candidate.to_string().chars() {
|
||||
for c in candidate.borrow().to_string().chars() {
|
||||
candidate_chars.push(c);
|
||||
lowercase_candidate_chars.append(&mut c.to_lowercase().collect::<Vec<_>>());
|
||||
}
|
||||
@@ -98,7 +100,11 @@ impl<'a> Matcher<'a> {
|
||||
);
|
||||
|
||||
if score > 0.0 {
|
||||
results.push(build_match(&candidate, score, &self.match_positions));
|
||||
results.push(build_match(
|
||||
candidate.borrow(),
|
||||
score,
|
||||
&self.match_positions,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use gpui::BackgroundExecutor;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
borrow::{Borrow, Cow},
|
||||
cmp::{self, Ordering},
|
||||
iter,
|
||||
ops::Range,
|
||||
@@ -113,14 +113,17 @@ impl Ord for StringMatch {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn match_strings(
|
||||
candidates: &[StringMatchCandidate],
|
||||
pub async fn match_strings<T>(
|
||||
candidates: &[T],
|
||||
query: &str,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: &AtomicBool,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Vec<StringMatch> {
|
||||
) -> Vec<StringMatch>
|
||||
where
|
||||
T: Borrow<StringMatchCandidate> + Sync,
|
||||
{
|
||||
if candidates.is_empty() || max_results == 0 {
|
||||
return Default::default();
|
||||
}
|
||||
@@ -129,10 +132,10 @@ pub async fn match_strings(
|
||||
return candidates
|
||||
.iter()
|
||||
.map(|candidate| StringMatch {
|
||||
candidate_id: candidate.id,
|
||||
candidate_id: candidate.borrow().id,
|
||||
score: 0.,
|
||||
positions: Default::default(),
|
||||
string: candidate.string.clone(),
|
||||
string: candidate.borrow().string.clone(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@@ -163,10 +166,12 @@ pub async fn match_strings(
|
||||
matcher.match_candidates(
|
||||
&[],
|
||||
&[],
|
||||
candidates[segment_start..segment_end].iter(),
|
||||
candidates[segment_start..segment_end]
|
||||
.iter()
|
||||
.map(|c| c.borrow()),
|
||||
results,
|
||||
cancel_flag,
|
||||
|candidate, score, positions| StringMatch {
|
||||
|candidate: &&StringMatchCandidate, score, positions| StringMatch {
|
||||
candidate_id: candidate.id,
|
||||
score,
|
||||
positions: positions.clone(),
|
||||
|
||||
@@ -34,6 +34,7 @@ text.workspace = true
|
||||
time.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -26,6 +26,7 @@ pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git
|
||||
pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore"));
|
||||
pub static FSMONITOR_DAEMON: LazyLock<&'static OsStr> =
|
||||
LazyLock::new(|| OsStr::new("fsmonitor--daemon"));
|
||||
pub static LFS_DIR: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("lfs"));
|
||||
pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
|
||||
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
|
||||
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
|
||||
@@ -35,23 +36,15 @@ pub struct Push {
|
||||
pub options: Option<PushOptions>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct StageAndNext {
|
||||
pub whole_excerpt: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct UnstageAndNext {
|
||||
pub whole_excerpt: bool,
|
||||
}
|
||||
|
||||
impl_actions!(git, [Push, StageAndNext, UnstageAndNext]);
|
||||
impl_actions!(git, [Push]);
|
||||
|
||||
actions!(
|
||||
git,
|
||||
[
|
||||
// per-hunk
|
||||
ToggleStaged,
|
||||
StageAndNext,
|
||||
UnstageAndNext,
|
||||
// per-file
|
||||
StageFile,
|
||||
UnstageFile,
|
||||
@@ -64,7 +57,7 @@ actions!(
|
||||
Pull,
|
||||
Fetch,
|
||||
Commit,
|
||||
ExpandCommitEditor,
|
||||
ShowCommitEditor,
|
||||
]
|
||||
);
|
||||
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||
|
||||
@@ -11,6 +11,8 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Borrow;
|
||||
use std::io::Write as _;
|
||||
#[cfg(not(windows))]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::process::Stdio;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
@@ -61,6 +63,12 @@ pub enum UpstreamTracking {
|
||||
Tracked(UpstreamTrackingStatus),
|
||||
}
|
||||
|
||||
impl From<UpstreamTrackingStatus> for UpstreamTracking {
|
||||
fn from(status: UpstreamTrackingStatus) -> Self {
|
||||
UpstreamTracking::Tracked(status)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpstreamTracking {
|
||||
pub fn is_gone(&self) -> bool {
|
||||
matches!(self, UpstreamTracking::Gone)
|
||||
@@ -74,9 +82,15 @@ impl UpstreamTracking {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpstreamTrackingStatus> for UpstreamTracking {
|
||||
fn from(status: UpstreamTrackingStatus) -> Self {
|
||||
UpstreamTracking::Tracked(status)
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteCommandOutput {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
impl RemoteCommandOutput {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.stdout.is_empty() && self.stderr.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,10 +199,10 @@ pub trait GitRepository: Send + Sync {
|
||||
branch_name: &str,
|
||||
upstream_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
) -> Result<()>;
|
||||
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<()>;
|
||||
) -> Result<RemoteCommandOutput>;
|
||||
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<RemoteCommandOutput>;
|
||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
|
||||
fn fetch(&self) -> Result<()>;
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
|
||||
@@ -611,19 +625,30 @@ impl GitRepository for RealGitRepository {
|
||||
branch_name: &str,
|
||||
remote_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
) -> Result<()> {
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
command
|
||||
.current_dir(&working_directory)
|
||||
.args(["push", "--quiet"])
|
||||
.args(["push"])
|
||||
.args(options.map(|option| match option {
|
||||
PushOptions::SetUpstream => "--set-upstream",
|
||||
PushOptions::Force => "--force-with-lease",
|
||||
}))
|
||||
.arg(remote_name)
|
||||
.arg(format!("{}:{}", branch_name, branch_name))
|
||||
.output()?;
|
||||
.arg(format!("{}:{}", branch_name, branch_name));
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
@@ -631,19 +656,33 @@ impl GitRepository for RealGitRepository {
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
Ok(())
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> {
|
||||
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
command
|
||||
.current_dir(&working_directory)
|
||||
.args(["pull", "--quiet"])
|
||||
.args(["pull"])
|
||||
.arg(remote_name)
|
||||
.arg(branch_name)
|
||||
.output()?;
|
||||
.arg(branch_name);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
@@ -651,17 +690,31 @@ impl GitRepository for RealGitRepository {
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(());
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<()> {
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
command
|
||||
.current_dir(&working_directory)
|
||||
.args(["fetch", "--quiet", "--all"])
|
||||
.output()?;
|
||||
.args(["fetch", "--all"]);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
@@ -669,7 +722,10 @@ impl GitRepository for RealGitRepository {
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(());
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,6 +772,18 @@ impl GitRepository for RealGitRepository {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn setup_askpass() -> Result<(PathBuf, tempfile::TempDir), anyhow::Error> {
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-git-askpass")
|
||||
.tempdir()?;
|
||||
let askpass_script = "#!/bin/sh\necho ''";
|
||||
let askpass_script_path = temp_dir.path().join("git-askpass.sh");
|
||||
std::fs::write(&askpass_script_path, askpass_script)?;
|
||||
std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755))?;
|
||||
Ok((askpass_script_path, temp_dir))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FakeGitRepository {
|
||||
state: Arc<Mutex<FakeGitRepositoryState>>,
|
||||
@@ -899,15 +967,20 @@ impl GitRepository for FakeGitRepository {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn push(&self, _branch: &str, _remote: &str, _options: Option<PushOptions>) -> Result<()> {
|
||||
fn push(
|
||||
&self,
|
||||
_branch: &str,
|
||||
_remote: &str,
|
||||
_options: Option<PushOptions>,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn pull(&self, _branch: &str, _remote: &str) -> Result<()> {
|
||||
fn pull(&self, _branch: &str, _remote: &str) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<()> {
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ git.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
panel.workspace = true
|
||||
@@ -55,6 +57,8 @@ zed_actions.workspace = true
|
||||
windows.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Context as _;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
|
||||
use git::repository::Branch;
|
||||
use gpui::{
|
||||
rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Project, ProjectPath};
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle, TriggerablePopover,
|
||||
};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::DetachAndPromptErr;
|
||||
use workspace::{ModalView, Workspace};
|
||||
@@ -31,35 +29,16 @@ pub fn open(
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let project = workspace.project().clone();
|
||||
let this = cx.entity();
|
||||
let style = BranchListStyle::Modal;
|
||||
cx.spawn_in(window, |_, mut cx| async move {
|
||||
// Modal branch picker has a longer trailoff than a popover one.
|
||||
let delegate = BranchListDelegate::new(project.clone(), style, 70, &cx).await?;
|
||||
|
||||
this.update_in(&mut cx, move |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
|
||||
let mut list = BranchList::new(project, style, 34., cx);
|
||||
list._subscription = Some(_subscription);
|
||||
list.picker = Some(picker);
|
||||
list
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
BranchList::new(project, style, 34., window, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
|
||||
}
|
||||
|
||||
pub fn popover(project: Entity<Project>, window: &mut Window, cx: &mut App) -> Entity<BranchList> {
|
||||
cx.new(|cx| {
|
||||
let mut list = BranchList::new(project, BranchListStyle::Popover, 15., cx);
|
||||
list.reload_branches(window, cx);
|
||||
let list = BranchList::new(project, BranchListStyle::Popover, 15., window, cx);
|
||||
list.focus_handle(cx).focus(window);
|
||||
list
|
||||
})
|
||||
}
|
||||
@@ -72,59 +51,54 @@ enum BranchListStyle {
|
||||
|
||||
pub struct BranchList {
|
||||
rem_width: f32,
|
||||
popover_handle: PopoverMenuHandle<Self>,
|
||||
default_focus_handle: FocusHandle,
|
||||
project: Entity<Project>,
|
||||
style: BranchListStyle,
|
||||
pub picker: Option<Entity<Picker<BranchListDelegate>>>,
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl TriggerablePopover for BranchList {
|
||||
fn menu_handle(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) -> PopoverMenuHandle<Self> {
|
||||
self.popover_handle.clone()
|
||||
}
|
||||
pub popover_handle: PopoverMenuHandle<Self>,
|
||||
pub picker: Entity<Picker<BranchListDelegate>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl BranchList {
|
||||
fn new(project: Entity<Project>, style: BranchListStyle, rem_width: f32, cx: &mut App) -> Self {
|
||||
fn new(
|
||||
project_handle: Entity<Project>,
|
||||
style: BranchListStyle,
|
||||
rem_width: f32,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let popover_handle = PopoverMenuHandle::default();
|
||||
Self {
|
||||
project,
|
||||
picker: None,
|
||||
rem_width,
|
||||
popover_handle,
|
||||
default_focus_handle: cx.focus_handle(),
|
||||
style,
|
||||
_subscription: None,
|
||||
}
|
||||
}
|
||||
let project = project_handle.read(cx);
|
||||
let all_branches_request = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| project.branches(ProjectPath::root_path(worktree.read(cx).id()), cx))
|
||||
.context("No worktrees found");
|
||||
|
||||
fn reload_branches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let project = self.project.clone();
|
||||
let style = self.style;
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let delegate = BranchListDelegate::new(project, style, 20, &cx).await?;
|
||||
let picker =
|
||||
cx.new_window_entity(|window, cx| Picker::uniform_list(delegate, window, cx))?;
|
||||
let all_branches = all_branches_request?.await?;
|
||||
|
||||
this.update(&mut cx, |branch_list, cx| {
|
||||
let subscription =
|
||||
cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| cx.emit(DismissEvent));
|
||||
|
||||
branch_list.picker = Some(picker);
|
||||
branch_list._subscription = Some(subscription);
|
||||
|
||||
cx.notify();
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.all_branches = Some(all_branches);
|
||||
picker.refresh(window, cx);
|
||||
})
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
let delegate = BranchListDelegate::new(project_handle.clone(), style, 20);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
|
||||
Self {
|
||||
picker,
|
||||
rem_width,
|
||||
popover_handle,
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ModalView for BranchList {}
|
||||
@@ -132,10 +106,7 @@ impl EventEmitter<DismissEvent> for BranchList {}
|
||||
|
||||
impl Focusable for BranchList {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker
|
||||
.as_ref()
|
||||
.map(|picker| picker.focus_handle(cx))
|
||||
.unwrap_or_else(|| self.default_focus_handle.clone())
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,24 +114,13 @@ impl Render for BranchList {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(rems(self.rem_width))
|
||||
.map(|parent| match self.picker.as_ref() {
|
||||
Some(picker) => parent.child(picker.clone()).on_mouse_down_out({
|
||||
let picker = picker.clone();
|
||||
cx.listener(move |_, _, window, cx| {
|
||||
picker.update(cx, |this, cx| {
|
||||
this.cancel(&Default::default(), window, cx);
|
||||
})
|
||||
.child(self.picker.clone())
|
||||
.on_mouse_down_out({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.picker.update(cx, |this, cx| {
|
||||
this.cancel(&Default::default(), window, cx);
|
||||
})
|
||||
}),
|
||||
None => parent.child(
|
||||
h_flex()
|
||||
.id("branch-picker-error")
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.reload_branches(window, cx)),
|
||||
)
|
||||
.child("Could not load branches.")
|
||||
.child("Click to retry"),
|
||||
),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -184,7 +144,7 @@ impl BranchEntry {
|
||||
|
||||
pub struct BranchListDelegate {
|
||||
matches: Vec<BranchEntry>,
|
||||
all_branches: Vec<Branch>,
|
||||
all_branches: Option<Vec<Branch>>,
|
||||
project: Entity<Project>,
|
||||
style: BranchListStyle,
|
||||
selected_index: usize,
|
||||
@@ -194,33 +154,20 @@ pub struct BranchListDelegate {
|
||||
}
|
||||
|
||||
impl BranchListDelegate {
|
||||
async fn new(
|
||||
fn new(
|
||||
project: Entity<Project>,
|
||||
style: BranchListStyle,
|
||||
branch_name_trailoff_after: usize,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let all_branches_request = cx.update(|cx| {
|
||||
let project = project.read(cx);
|
||||
let first_worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.context("No worktrees found")?;
|
||||
let project_path = ProjectPath::root_path(first_worktree.read(cx).id());
|
||||
anyhow::Ok(project.branches(project_path, cx))
|
||||
})??;
|
||||
|
||||
let all_branches = all_branches_request.await?;
|
||||
|
||||
Ok(Self {
|
||||
) -> Self {
|
||||
Self {
|
||||
matches: vec![],
|
||||
project,
|
||||
style,
|
||||
all_branches,
|
||||
all_branches: None,
|
||||
selected_index: 0,
|
||||
last_query: Default::default(),
|
||||
branch_name_trailoff_after,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn branch_count(&self) -> usize {
|
||||
@@ -261,32 +208,31 @@ impl PickerDelegate for BranchListDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(mut all_branches) = self.all_branches.clone() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
cx.spawn_in(window, move |picker, mut cx| async move {
|
||||
let candidates = picker.update(&mut cx, |picker, _| {
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
let mut branches = picker.delegate.all_branches.clone();
|
||||
if query.is_empty() {
|
||||
if branches.len() > RECENT_BRANCHES_COUNT {
|
||||
// Truncate list of recent branches
|
||||
// Do a partial sort to show recent-ish branches first.
|
||||
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||
rhs.priority_key().cmp(&lhs.priority_key())
|
||||
});
|
||||
branches.truncate(RECENT_BRANCHES_COUNT);
|
||||
}
|
||||
branches.sort_unstable_by(|lhs, rhs| {
|
||||
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
if query.is_empty() {
|
||||
if all_branches.len() > RECENT_BRANCHES_COUNT {
|
||||
// Truncate list of recent branches
|
||||
// Do a partial sort to show recent-ish branches first.
|
||||
all_branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||
rhs.priority_key().cmp(&lhs.priority_key())
|
||||
});
|
||||
all_branches.truncate(RECENT_BRANCHES_COUNT);
|
||||
}
|
||||
branches
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.collect::<Vec<StringMatchCandidate>>()
|
||||
});
|
||||
let Some(candidates) = candidates.log_err() else {
|
||||
return;
|
||||
};
|
||||
all_branches.sort_unstable_by(|lhs, rhs| {
|
||||
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
|
||||
});
|
||||
}
|
||||
|
||||
let candidates = all_branches
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
let matches: Vec<BranchEntry> = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
use crate::branch_picker::{self, BranchList};
|
||||
use crate::git_panel::{commit_message_editor, GitPanel};
|
||||
use git::{Commit, ExpandCommitEditor};
|
||||
use git::{Commit, ShowCommitEditor};
|
||||
use panel::{panel_button, panel_editor_style, panel_filled_button};
|
||||
use project::Project;
|
||||
use ui::{prelude::*, KeybindingHint, PopoverButton, Tooltip, TriggerablePopover};
|
||||
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
|
||||
|
||||
use editor::{Editor, EditorElement};
|
||||
use gpui::*;
|
||||
@@ -110,32 +110,14 @@ struct RestoreDock {
|
||||
|
||||
impl CommitModal {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context<Workspace>) {
|
||||
workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| {
|
||||
workspace.register_action(|workspace, _: &ShowCommitEditor, window, cx| {
|
||||
let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (can_commit, conflict) = git_panel.update(cx, |git_panel, cx| {
|
||||
let can_commit = git_panel.can_commit();
|
||||
let conflict = git_panel.has_unstaged_conflicts();
|
||||
if can_commit {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
}
|
||||
(can_commit, conflict)
|
||||
git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
});
|
||||
if !can_commit {
|
||||
let message = if conflict {
|
||||
"There are still conflicts. You must stage these before committing."
|
||||
} else {
|
||||
"No changes to commit."
|
||||
};
|
||||
let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
let dock = workspace.dock_at_position(git_panel.position(window, cx));
|
||||
let is_open = dock.read(cx).is_open();
|
||||
@@ -163,30 +145,30 @@ impl CommitModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = git_panel.read(cx);
|
||||
let suggested_message = panel.suggest_commit_message();
|
||||
let suggested_commit_message = panel.suggest_commit_message();
|
||||
|
||||
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
let buffer = git_panel.commit_message_buffer(cx).clone();
|
||||
let panel_editor = git_panel.commit_editor.clone();
|
||||
let project = git_panel.project.clone();
|
||||
cx.new(|cx| commit_message_editor(buffer, None, project.clone(), false, window, cx))
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut editor =
|
||||
commit_message_editor(buffer, None, project.clone(), false, window, cx);
|
||||
editor.sync_selections(panel_editor, cx).detach();
|
||||
|
||||
editor
|
||||
})
|
||||
});
|
||||
|
||||
let commit_message = commit_editor.read(cx).text(cx);
|
||||
|
||||
if let Some(suggested_message) = suggested_message {
|
||||
if let Some(suggested_commit_message) = suggested_commit_message {
|
||||
if commit_message.is_empty() {
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(suggested_message, window, cx);
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
editor.set_placeholder_text(suggested_commit_message, cx);
|
||||
});
|
||||
} else {
|
||||
if commit_message.as_str().trim() == suggested_message.trim() {
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
// select the message to make it easy to delete
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +232,7 @@ impl CommitModal {
|
||||
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let git_panel = self.git_panel.clone();
|
||||
|
||||
let (branch, tooltip, commit_label, co_authors) =
|
||||
let (branch, can_commit, tooltip, commit_label, co_authors) =
|
||||
self.git_panel.update(cx, |git_panel, cx| {
|
||||
let branch = git_panel
|
||||
.active_repository
|
||||
@@ -262,18 +244,10 @@ impl CommitModal {
|
||||
.map(|b| b.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "<no branch>".into());
|
||||
let tooltip = if git_panel.has_staged_changes() {
|
||||
"Commit staged changes"
|
||||
} else {
|
||||
"Commit changes to tracked files"
|
||||
};
|
||||
let title = if git_panel.has_staged_changes() {
|
||||
"Commit"
|
||||
} else {
|
||||
"Commit All"
|
||||
};
|
||||
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
|
||||
let title = git_panel.commit_button_title();
|
||||
let co_authors = git_panel.render_co_authors(cx);
|
||||
(branch, tooltip, title, co_authors)
|
||||
(branch, can_commit, tooltip, title, co_authors)
|
||||
});
|
||||
|
||||
let branch_picker_button = panel_button(branch)
|
||||
@@ -291,12 +265,20 @@ impl CommitModal {
|
||||
}))
|
||||
.style(ButtonStyle::Transparent);
|
||||
|
||||
let branch_picker = PopoverButton::new(
|
||||
self.branch_list.clone(),
|
||||
Corner::BottomLeft,
|
||||
branch_picker_button,
|
||||
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
||||
);
|
||||
let branch_picker = PopoverMenu::new("popover-button")
|
||||
.menu({
|
||||
let branch_list = self.branch_list.clone();
|
||||
move |_window, _cx| Some(branch_list.clone())
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
branch_picker_button,
|
||||
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
||||
)
|
||||
.anchor(Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
});
|
||||
|
||||
let close_kb_hint =
|
||||
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
|
||||
@@ -308,9 +290,8 @@ impl CommitModal {
|
||||
None
|
||||
};
|
||||
|
||||
let (panel_editor_focus_handle, can_commit) = git_panel.update(cx, |git_panel, cx| {
|
||||
(git_panel.editor_focus_handle(cx), git_panel.can_commit())
|
||||
});
|
||||
let panel_editor_focus_handle =
|
||||
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
|
||||
|
||||
let commit_button = panel_filled_button(commit_label)
|
||||
.tooltip(move |window, cx| {
|
||||
@@ -332,12 +313,7 @@ impl CommitModal {
|
||||
.w_full()
|
||||
.h(px(self.properties.footer_height))
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(branch_picker.render(window, cx))
|
||||
.children(co_authors),
|
||||
)
|
||||
.child(h_flex().gap_1().child(branch_picker).children(co_authors))
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -354,6 +330,7 @@ impl CommitModal {
|
||||
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.git_panel
|
||||
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
|
||||
@@ -377,7 +354,7 @@ impl Render for CommitModal {
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
|
||||
this.branch_list.update(cx, |branch_list, cx| {
|
||||
branch_list.menu_handle(window, cx).toggle(window, cx);
|
||||
branch_list.popover_handle.toggle(window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ use git_panel_settings::GitPanelSettings;
|
||||
use gpui::App;
|
||||
use project_diff::ProjectDiff;
|
||||
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub mod branch_picker;
|
||||
mod commit_modal;
|
||||
@@ -11,6 +12,7 @@ pub mod git_panel;
|
||||
mod git_panel_settings;
|
||||
pub mod picker_prompt;
|
||||
pub mod project_diff;
|
||||
mod remote_output_toast;
|
||||
pub mod repository_selector;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -18,6 +20,34 @@ pub fn init(cx: &mut App) {
|
||||
branch_picker::init(cx);
|
||||
cx.observe_new(ProjectDiff::register).detach();
|
||||
commit_modal::init(cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, fetch: &git::Fetch, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.fetch(fetch, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, push: &git::Push, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(push, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, pull: &git::Pull, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.pull(pull, window, cx);
|
||||
});
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
// TODO: Add updated status colors to theme
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn prompt(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<usize>> {
|
||||
) -> Task<Result<Option<usize>>> {
|
||||
if options.is_empty() {
|
||||
return Task::ready(Err(anyhow!("No options")));
|
||||
}
|
||||
@@ -43,7 +43,10 @@ pub fn prompt(
|
||||
})
|
||||
})?;
|
||||
|
||||
rx.await?
|
||||
match rx.await {
|
||||
Ok(selection) => Some(selection).transpose(),
|
||||
Err(_) => anyhow::Ok(None), // User cancelled
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@ use anyhow::Result;
|
||||
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
|
||||
use collections::HashSet;
|
||||
use editor::{
|
||||
actions::{GoToHunk, GoToPrevHunk},
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ToPoint,
|
||||
Editor, EditorEvent,
|
||||
};
|
||||
use feature_flags::FeatureFlagViewExt;
|
||||
use futures::StreamExt;
|
||||
use git::{
|
||||
status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
|
||||
status::FileStatus, ShowCommitEditor, StageAll, StageAndNext, ToggleStaged, UnstageAll,
|
||||
UnstageAndNext,
|
||||
};
|
||||
use gpui::{
|
||||
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
|
||||
@@ -191,6 +192,19 @@ impl ProjectDiff {
|
||||
self.move_to_path(path_key, window, cx)
|
||||
}
|
||||
|
||||
pub fn active_path(&self, cx: &App) -> Option<ProjectPath> {
|
||||
let editor = self.editor.read(cx);
|
||||
let position = editor.selections.newest_anchor().head();
|
||||
let multi_buffer = editor.buffer().read(cx);
|
||||
let (_, buffer, _) = multi_buffer.excerpt_containing(position, cx)?;
|
||||
|
||||
let file = buffer.read(cx).file()?;
|
||||
Some(ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
@@ -243,14 +257,12 @@ impl ProjectDiff {
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut commit = false;
|
||||
let mut stage_all = false;
|
||||
let mut unstage_all = false;
|
||||
self.workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
|
||||
let git_panel = git_panel.read(cx);
|
||||
commit = git_panel.can_commit();
|
||||
stage_all = git_panel.can_stage_all();
|
||||
unstage_all = git_panel.can_unstage_all();
|
||||
}
|
||||
@@ -262,7 +274,6 @@ impl ProjectDiff {
|
||||
unstage: has_staged_hunks,
|
||||
prev_next,
|
||||
selection,
|
||||
commit,
|
||||
stage_all,
|
||||
unstage_all,
|
||||
};
|
||||
@@ -270,41 +281,26 @@ impl ProjectDiff {
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
_: &Entity<Editor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::ScrollPositionChanged { .. } => editor.update(cx, |editor, cx| {
|
||||
let anchor = editor.scroll_manager.anchor().anchor;
|
||||
let multibuffer = self.multibuffer.read(cx);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
let mut point = anchor.to_point(&snapshot);
|
||||
point.row = (point.row + 1).min(snapshot.max_row().0);
|
||||
point.column = 0;
|
||||
|
||||
let Some((_, buffer, _)) = self.multibuffer.read(cx).excerpt_containing(point, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(project_path) = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| (file.worktree_id(cx), file.path().clone()))
|
||||
else {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let Some(project_path) = self.active_path(cx) else {
|
||||
return;
|
||||
};
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
|
||||
git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.select_entry_by_path(project_path.into(), window, cx)
|
||||
git_panel.select_entry_by_path(project_path, window, cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -399,6 +395,7 @@ impl ProjectDiff {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if was_empty {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
// TODO select the very beginning (possibly inside a deletion)
|
||||
selections.select_ranges([0..0])
|
||||
});
|
||||
}
|
||||
@@ -773,7 +770,6 @@ struct ButtonStates {
|
||||
selection: bool,
|
||||
stage_all: bool,
|
||||
unstage_all: bool,
|
||||
commit: bool,
|
||||
}
|
||||
|
||||
impl Render for ProjectDiffToolbar {
|
||||
@@ -812,10 +808,8 @@ impl Render for ProjectDiffToolbar {
|
||||
el.child(
|
||||
Button::new("stage", "Stage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Stage",
|
||||
&StageAndNext {
|
||||
whole_excerpt: false,
|
||||
},
|
||||
"Stage and go to next hunk",
|
||||
&StageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
// don't actually disable the button so it's mashable
|
||||
@@ -825,22 +819,14 @@ impl Render for ProjectDiffToolbar {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(
|
||||
&StageAndNext {
|
||||
whole_excerpt: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.dispatch_action(&StageAndNext, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("unstage", "Unstage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Unstage",
|
||||
&UnstageAndNext {
|
||||
whole_excerpt: false,
|
||||
},
|
||||
"Unstage and go to next hunk",
|
||||
&UnstageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
.color(if button_states.unstage {
|
||||
@@ -849,13 +835,7 @@ impl Render for ProjectDiffToolbar {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(
|
||||
&UnstageAndNext {
|
||||
whole_excerpt: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.dispatch_action(&UnstageAndNext, window, cx)
|
||||
})),
|
||||
)
|
||||
}),
|
||||
@@ -869,12 +849,12 @@ impl Render for ProjectDiffToolbar {
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Go to previous hunk",
|
||||
&GoToPrevHunk,
|
||||
&GoToPreviousHunk,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.prev_next)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&GoToPrevHunk, window, cx)
|
||||
this.dispatch_action(&GoToPreviousHunk, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
@@ -933,26 +913,27 @@ impl Render for ProjectDiffToolbar {
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit")
|
||||
.disabled(!button_states.commit)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Commit",
|
||||
&Commit,
|
||||
&ShowCommitEditor,
|
||||
&focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&Commit, window, cx);
|
||||
this.dispatch_action(&ShowCommitEditor, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::test::editor_test_context::assert_state_with_diff;
|
||||
use db::indoc;
|
||||
use editor::test::editor_test_context::{assert_state_with_diff, EditorTestContext};
|
||||
use git::status::{StatusCode, TrackedStatus};
|
||||
use gpui::TestAppContext;
|
||||
use project::FakeFs;
|
||||
@@ -963,6 +944,11 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
env_logger::init();
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
@@ -1135,4 +1121,196 @@ mod tests {
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hunks_after_restore_then_modify(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"foo": "modified\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/project/foo"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
|
||||
});
|
||||
let diff = cx.new_window_entity(|window, cx| {
|
||||
ProjectDiff::new(project.clone(), workspace, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("foo".into(), "original\n".into())],
|
||||
);
|
||||
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
|
||||
state.statuses = HashMap::from_iter([(
|
||||
"foo".into(),
|
||||
TrackedStatus {
|
||||
index_status: StatusCode::Unmodified,
|
||||
worktree_status: StatusCode::Modified,
|
||||
}
|
||||
.into(),
|
||||
)]);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let diff_editor = diff.update(cx, |diff, _| diff.editor.clone());
|
||||
|
||||
assert_state_with_diff(
|
||||
&diff_editor,
|
||||
cx,
|
||||
&"
|
||||
- original
|
||||
+ ˇmodified
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
let prev_buffer_hunks =
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
let snapshot = buffer_editor.snapshot(window, cx);
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let prev_buffer_hunks = buffer_editor
|
||||
.diff_hunks_in_ranges(&[editor::Anchor::min()..editor::Anchor::max()], snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
buffer_editor.git_restore(&Default::default(), window, cx);
|
||||
prev_buffer_hunks
|
||||
});
|
||||
assert_eq!(prev_buffer_hunks.len(), 1);
|
||||
cx.run_until_parked();
|
||||
|
||||
let new_buffer_hunks =
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
let snapshot = buffer_editor.snapshot(window, cx);
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let new_buffer_hunks = buffer_editor
|
||||
.diff_hunks_in_ranges(&[editor::Anchor::min()..editor::Anchor::max()], snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
buffer_editor.git_restore(&Default::default(), window, cx);
|
||||
new_buffer_hunks
|
||||
});
|
||||
assert_eq!(new_buffer_hunks.as_slice(), &[]);
|
||||
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
buffer_editor.set_text("different\n", window, cx);
|
||||
buffer_editor.save(false, project.clone(), window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_state_with_diff(
|
||||
&diff_editor,
|
||||
cx,
|
||||
&"
|
||||
- original
|
||||
+ ˇdifferent
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
use crate::project_diff::{self, ProjectDiff};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_to_prev_hunk_multibuffer(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git":{},
|
||||
"a.txt": "created\n",
|
||||
"b.txt": "really changed\n",
|
||||
"c.txt": "unchanged\n"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_git_content_for_repo(
|
||||
Path::new("/a/.git"),
|
||||
&[
|
||||
("b.txt".into(), "before\n".to_string(), None),
|
||||
("c.txt".into(), "unchanged\n".to_string(), None),
|
||||
("d.txt".into(), "deleted\n".to_string(), None),
|
||||
],
|
||||
);
|
||||
|
||||
let project = Project::test(fs, [Path::new("/a")], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.focus(&workspace);
|
||||
cx.update(|window, cx| {
|
||||
window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let item = workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_item_as::<ProjectDiff>(cx).unwrap()
|
||||
});
|
||||
cx.focus(&item);
|
||||
let editor = item.update(cx, |item, _| item.editor.clone());
|
||||
|
||||
let mut cx = EditorTestContext::for_editor_in(editor, cx).await;
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc!(
|
||||
"
|
||||
[EXCERPT]
|
||||
before
|
||||
really changed
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇcreated
|
||||
"
|
||||
));
|
||||
|
||||
cx.dispatch_action(editor::actions::GoToPreviousHunk);
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc!(
|
||||
"
|
||||
[EXCERPT]
|
||||
before
|
||||
really changed
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
created
|
||||
"
|
||||
));
|
||||
|
||||
cx.dispatch_action(editor::actions::GoToPreviousHunk);
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc!(
|
||||
"
|
||||
[EXCERPT]
|
||||
ˇbefore
|
||||
really changed
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
created
|
||||
"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
227
crates/git_ui/src/remote_output_toast.rs
Normal file
227
crates/git_ui/src/remote_output_toast.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use std::{ops::Range, time::Duration};
|
||||
|
||||
use git::repository::{Remote, RemoteCommandOutput};
|
||||
use gpui::{
|
||||
DismissEvent, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveText,
|
||||
StyledText, Task, UnderlineStyle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use ui::{
|
||||
div, h_flex, px, v_flex, vh, Clickable, Color, Context, FluentBuilder, Icon, IconButton,
|
||||
IconName, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
|
||||
Render, SharedString, Styled, StyledExt, Window,
|
||||
};
|
||||
use workspace::{
|
||||
notifications::{Notification, NotificationId},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
pub enum RemoteAction {
|
||||
Fetch,
|
||||
Pull,
|
||||
Push(Remote),
|
||||
}
|
||||
|
||||
struct InfoFromRemote {
|
||||
name: SharedString,
|
||||
remote_text: SharedString,
|
||||
links: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
pub struct RemoteOutputToast {
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
_id: NotificationId,
|
||||
message: SharedString,
|
||||
remote_info: Option<InfoFromRemote>,
|
||||
_dismiss_task: Task<()>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for RemoteOutputToast {
|
||||
fn focus_handle(&self, _cx: &ui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification for RemoteOutputToast {}
|
||||
|
||||
const REMOTE_OUTPUT_TOAST_SECONDS: u64 = 5;
|
||||
|
||||
impl RemoteOutputToast {
|
||||
pub fn new(
|
||||
action: RemoteAction,
|
||||
output: RemoteCommandOutput,
|
||||
id: NotificationId,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let task = cx.spawn({
|
||||
let workspace = workspace.clone();
|
||||
let id = id.clone();
|
||||
|_, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs(REMOTE_OUTPUT_TOAST_SECONDS))
|
||||
.await;
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.dismiss_notification(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
let mut message: SharedString;
|
||||
let remote;
|
||||
|
||||
match action {
|
||||
RemoteAction::Fetch | RemoteAction::Pull => {
|
||||
if output.is_empty() {
|
||||
message = "Up to date".into();
|
||||
} else {
|
||||
message = output.stderr.into();
|
||||
}
|
||||
remote = None;
|
||||
}
|
||||
|
||||
RemoteAction::Push(remote_ref) => {
|
||||
message = output.stdout.trim().to_string().into();
|
||||
if message.is_empty() {
|
||||
message = output.stderr.trim().to_string().into();
|
||||
if message.is_empty() {
|
||||
message = "Push Successful".into();
|
||||
}
|
||||
remote = None;
|
||||
} else {
|
||||
let remote_message = get_remote_lines(&output.stderr);
|
||||
|
||||
remote = if remote_message.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let finder = LinkFinder::new();
|
||||
let links = finder
|
||||
.links(&remote_message)
|
||||
.filter(|link| *link.kind() == LinkKind::Url)
|
||||
.map(|link| link.start()..link.end())
|
||||
.collect_vec();
|
||||
|
||||
Some(InfoFromRemote {
|
||||
name: remote_ref.name,
|
||||
remote_text: remote_message.into(),
|
||||
links,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
_workspace: workspace,
|
||||
_id: id,
|
||||
message,
|
||||
remote_info: remote,
|
||||
_dismiss_task: task,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RemoteOutputToast {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
div()
|
||||
.occlude()
|
||||
.w_full()
|
||||
.max_h(vh(0.8, window))
|
||||
.elevation_3(cx)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::GitBranch).color(Color::Default))
|
||||
.child(Label::new("Git")),
|
||||
)
|
||||
.child(h_flex().child(
|
||||
IconButton::new("close", IconName::Close).on_click(
|
||||
cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(Label::new(self.message.clone()).size(LabelSize::Default))
|
||||
.when_some(self.remote_info.as_ref(), |this, remote_info| {
|
||||
this.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(Color::Muted.color(cx))
|
||||
.rounded_lg()
|
||||
.text_sm()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Cloud).color(Color::Default))
|
||||
.child(
|
||||
Label::new(remote_info.name.clone())
|
||||
.size(LabelSize::Default),
|
||||
),
|
||||
)
|
||||
.map(|div| {
|
||||
let styled_text =
|
||||
StyledText::new(remote_info.remote_text.clone())
|
||||
.with_highlights(remote_info.links.iter().map(
|
||||
|link| {
|
||||
(
|
||||
link.clone(),
|
||||
HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: px(1.0),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
));
|
||||
let this = cx.weak_entity();
|
||||
let text = InteractiveText::new("remote-message", styled_text)
|
||||
.on_click(
|
||||
remote_info.links.clone(),
|
||||
move |ix, _window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(remote_info) = &this.remote_info {
|
||||
cx.open_url(
|
||||
&remote_info.remote_text
|
||||
[remote_info.links[ix].clone()],
|
||||
)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
);
|
||||
|
||||
div.child(text)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RemoteOutputToast {}
|
||||
|
||||
fn get_remote_lines(output: &str) -> String {
|
||||
output
|
||||
.lines()
|
||||
.filter_map(|line| line.strip_prefix("remote:"))
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, Task, WeakEntity,
|
||||
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
|
||||
Task, WeakEntity,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{
|
||||
@@ -8,7 +8,7 @@ use project::{
|
||||
Project,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
|
||||
pub struct RepositorySelector {
|
||||
picker: Entity<Picker<RepositorySelectorDelegate>>,
|
||||
@@ -47,10 +47,6 @@ impl RepositorySelector {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn repositories_len(&self, cx: &App) -> usize {
|
||||
self.picker.read(cx).delegate.repository_entries.len()
|
||||
}
|
||||
|
||||
fn handle_project_git_event(
|
||||
&mut self,
|
||||
git_store: &Entity<GitStore>,
|
||||
@@ -82,54 +78,6 @@ impl Render for RepositorySelector {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct RepositorySelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
repository_selector: Entity<RepositorySelector>,
|
||||
trigger: T,
|
||||
tooltip: TT,
|
||||
handle: Option<PopoverMenuHandle<RepositorySelector>>,
|
||||
}
|
||||
|
||||
impl<T, TT> RepositorySelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
pub fn new(repository_selector: Entity<RepositorySelector>, trigger: T, tooltip: TT) -> Self {
|
||||
Self {
|
||||
repository_selector,
|
||||
trigger,
|
||||
tooltip,
|
||||
handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<RepositorySelector>) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, TT> RenderOnce for RepositorySelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let repository_selector = self.repository_selector.clone();
|
||||
|
||||
PopoverMenu::new("repository-switcher")
|
||||
.menu(move |_window, _cx| Some(repository_selector.clone()))
|
||||
.trigger_with_tooltip(self.trigger, self.tooltip)
|
||||
.attach(gpui::Corner::BottomLeft)
|
||||
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RepositorySelectorDelegate {
|
||||
project: WeakEntity<Project>,
|
||||
repository_selector: WeakEntity<RepositorySelector>,
|
||||
|
||||
@@ -74,6 +74,9 @@ struct ImageShowcase {
|
||||
impl Render for ImageShowcase {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("main")
|
||||
.overflow_y_scroll()
|
||||
.p_5()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -116,9 +119,21 @@ impl Render for ImageShowcase {
|
||||
div()
|
||||
.flex_col()
|
||||
.child("Auto Height")
|
||||
.child(img("https://picsum.photos/480/640").w(px(180.))),
|
||||
.child(img("https://picsum.photos/800/400").w(px(180.))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.border_1()
|
||||
.border_color(rgb(0xC0C0C0))
|
||||
.child("image with max width 100%")
|
||||
.child(img("https://picsum.photos/800/400").max_w_full()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +179,7 @@ fn main() {
|
||||
cx.new(|_| ImageShowcase {
|
||||
// Relative path to your root project path
|
||||
local_resource: manifest_dir.join("examples/image/app-icon.png").into(),
|
||||
|
||||
remote_resource: "https://picsum.photos/512/512".into(),
|
||||
|
||||
remote_resource: "https://picsum.photos/800/400".into(),
|
||||
asset_resource: "image/color.svg".into(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -90,6 +90,21 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribe to an event type from ourself
|
||||
pub fn subscribe_self<Evt>(
|
||||
&mut self,
|
||||
mut on_event: impl FnMut(&mut T, &Evt, &mut Context<'_, T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static + EventEmitter<Evt>,
|
||||
Evt: 'static,
|
||||
{
|
||||
let this = self.entity();
|
||||
self.app.subscribe(&this, move |this, evt, cx| {
|
||||
this.update(cx, |this, cx| on_event(this, evt, cx))
|
||||
})
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when GPUI releases this entity.
|
||||
pub fn on_release(&self, on_release: impl FnOnce(&mut T, &mut App) + 'static) -> Subscription
|
||||
where
|
||||
|
||||
@@ -670,6 +670,14 @@ pub fn pattern_slash(color: Hsla, thickness: f32) -> Background {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a solid background color.
|
||||
pub fn solid_background(color: impl Into<Hsla>) -> Background {
|
||||
Background {
|
||||
solid: color.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a LinearGradient background color.
|
||||
///
|
||||
/// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.
|
||||
|
||||
@@ -1509,6 +1509,7 @@ impl Interactivity {
|
||||
|| self.tracked_focus_handle.is_some()
|
||||
|| self.hover_style.is_some()
|
||||
|| self.group_hover_style.is_some()
|
||||
|| self.hover_listener.is_some()
|
||||
|| !self.mouse_up_listeners.is_empty()
|
||||
|| !self.mouse_down_listeners.is_empty()
|
||||
|| !self.mouse_move_listeners.is_empty()
|
||||
@@ -2127,6 +2128,7 @@ impl Interactivity {
|
||||
if let Some(scroll_offset) = self.scroll_offset.clone() {
|
||||
let overflow = style.overflow;
|
||||
let allow_concurrent_scroll = style.allow_concurrent_scroll;
|
||||
let restrict_scroll_to_axis = style.restrict_scroll_to_axis;
|
||||
let line_height = window.line_height();
|
||||
let hitbox = hitbox.clone();
|
||||
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
|
||||
@@ -2139,7 +2141,7 @@ impl Interactivity {
|
||||
if overflow.x == Overflow::Scroll {
|
||||
if !delta.x.is_zero() {
|
||||
delta_x = delta.x;
|
||||
} else if overflow.y != Overflow::Scroll {
|
||||
} else if !restrict_scroll_to_axis && overflow.y != Overflow::Scroll {
|
||||
delta_x = delta.y;
|
||||
}
|
||||
}
|
||||
@@ -2147,7 +2149,7 @@ impl Interactivity {
|
||||
if overflow.y == Overflow::Scroll {
|
||||
if !delta.y.is_zero() {
|
||||
delta_y = delta.y;
|
||||
} else if overflow.x != Overflow::Scroll {
|
||||
} else if !restrict_scroll_to_axis && overflow.x != Overflow::Scroll {
|
||||
delta_y = delta.x;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +301,8 @@ impl Element for Img {
|
||||
}
|
||||
|
||||
let image_size = data.size(frame_index);
|
||||
style.aspect_ratio =
|
||||
Some(image_size.width.0 as f32 / image_size.height.0 as f32);
|
||||
|
||||
if let Length::Auto = style.size.width {
|
||||
style.size.width = match style.size.height {
|
||||
|
||||
@@ -137,6 +137,7 @@ impl IntoElement for SharedString {
|
||||
pub struct StyledText {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
||||
layout: TextLayout,
|
||||
}
|
||||
|
||||
@@ -146,6 +147,7 @@ impl StyledText {
|
||||
StyledText {
|
||||
text: text.into(),
|
||||
runs: None,
|
||||
delayed_highlights: None,
|
||||
layout: TextLayout::default(),
|
||||
}
|
||||
}
|
||||
@@ -157,11 +159,39 @@ impl StyledText {
|
||||
|
||||
/// Set the styling attributes for the given text, as well as
|
||||
/// as any ranges of text that have had their style customized.
|
||||
pub fn with_highlights(
|
||||
pub fn with_default_highlights(
|
||||
mut self,
|
||||
default_style: &TextStyle,
|
||||
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||
) -> Self {
|
||||
debug_assert!(
|
||||
self.delayed_highlights.is_none(),
|
||||
"Can't use `with_default_highlights` and `with_highlights`"
|
||||
);
|
||||
let runs = Self::compute_runs(&self.text, default_style, highlights);
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the styling attributes for the given text, as well as
|
||||
/// as any ranges of text that have had their style customized.
|
||||
pub fn with_highlights(
|
||||
mut self,
|
||||
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||
) -> Self {
|
||||
debug_assert!(
|
||||
self.runs.is_none(),
|
||||
"Can't use `with_highlights` and `with_default_highlights`"
|
||||
);
|
||||
self.delayed_highlights = Some(highlights.into_iter().collect::<Vec<_>>());
|
||||
self
|
||||
}
|
||||
|
||||
fn compute_runs(
|
||||
text: &str,
|
||||
default_style: &TextStyle,
|
||||
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||
) -> Vec<TextRun> {
|
||||
let mut runs = Vec::new();
|
||||
let mut ix = 0;
|
||||
for (range, highlight) in highlights {
|
||||
@@ -176,11 +206,10 @@ impl StyledText {
|
||||
);
|
||||
ix = range.end;
|
||||
}
|
||||
if ix < self.text.len() {
|
||||
runs.push(default_style.to_run(self.text.len() - ix));
|
||||
if ix < text.len() {
|
||||
runs.push(default_style.to_run(text.len() - ix));
|
||||
}
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
runs
|
||||
}
|
||||
|
||||
/// Set the text runs for this piece of text.
|
||||
@@ -200,15 +229,17 @@ impl Element for StyledText {
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
|
||||
_id: Option<&GlobalElementId>,
|
||||
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self
|
||||
.layout
|
||||
.layout(self.text.clone(), self.runs.take(), window, cx);
|
||||
let runs = self.runs.take().or_else(|| {
|
||||
self.delayed_highlights.take().map(|delayed_highlights| {
|
||||
Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
|
||||
})
|
||||
});
|
||||
|
||||
let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,28 @@ pub struct Style {
|
||||
pub scrollbar_width: f32,
|
||||
/// Whether both x and y axis should be scrollable at the same time.
|
||||
pub allow_concurrent_scroll: bool,
|
||||
/// Whether scrolling should be restricted to the axis indicated by the mouse wheel.
|
||||
///
|
||||
/// This means that:
|
||||
/// - The mouse wheel alone will only ever scroll the Y axis.
|
||||
/// - Holding `Shift` and using the mouse wheel will scroll the X axis.
|
||||
///
|
||||
/// ## Motivation
|
||||
///
|
||||
/// On the web when scrolling with the mouse wheel, scrolling up and down will always scroll the Y axis, even when
|
||||
/// the mouse is over a horizontally-scrollable element.
|
||||
///
|
||||
/// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis
|
||||
/// to the X axis.
|
||||
///
|
||||
/// Currently, GPUI operates differently from the web in that it will scroll an element in either the X or Y axis
|
||||
/// when scrolling with just the mouse wheel. This causes problems when scrolling in a vertical list that contains
|
||||
/// horizontally-scrollable elements, as when you get to the horizontally-scrollable elements the scroll will be
|
||||
/// hijacked.
|
||||
///
|
||||
/// Ideally we would match the web's behavior and not have a need for this, but right now we're adding this opt-in
|
||||
/// style property to limit the potential blast radius.
|
||||
pub restrict_scroll_to_axis: bool,
|
||||
|
||||
// Position properties
|
||||
/// What should the `position` value of this struct use as a base offset?
|
||||
@@ -702,6 +724,7 @@ impl Default for Style {
|
||||
y: Overflow::Visible,
|
||||
},
|
||||
allow_concurrent_scroll: false,
|
||||
restrict_scroll_to_axis: false,
|
||||
scrollbar_width: 0.0,
|
||||
position: Position::Relative,
|
||||
inset: Edges::auto(),
|
||||
|
||||
@@ -168,6 +168,23 @@ impl Subscription {
|
||||
pub fn detach(mut self) {
|
||||
self.unsubscribe.take();
|
||||
}
|
||||
|
||||
/// Joins two subscriptions into a single subscription. Detach will
|
||||
/// detach both interior subscriptions.
|
||||
pub fn join(mut subscription_a: Self, mut subscription_b: Self) -> Self {
|
||||
let a_unsubscribe = subscription_a.unsubscribe.take();
|
||||
let b_unsubscribe = subscription_b.unsubscribe.take();
|
||||
Self {
|
||||
unsubscribe: Some(Box::new(move || {
|
||||
if let Some(self_unsubscribe) = a_unsubscribe {
|
||||
self_unsubscribe();
|
||||
}
|
||||
if let Some(other_unsubscribe) = b_unsubscribe {
|
||||
other_unsubscribe();
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
|
||||
@@ -110,7 +110,7 @@ pub struct Buffer {
|
||||
pending_autoindent: Option<Task<()>>,
|
||||
sync_parse_timeout: Duration,
|
||||
syntax_map: Mutex<SyntaxMap>,
|
||||
parsing_in_background: bool,
|
||||
reparse: Option<Task<()>>,
|
||||
parse_status: (watch::Sender<ParseStatus>, watch::Receiver<ParseStatus>),
|
||||
non_text_state_update_count: usize,
|
||||
diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
|
||||
@@ -590,7 +590,7 @@ impl HighlightedText {
|
||||
|
||||
pub fn to_styled_text(&self, default_style: &TextStyle) -> StyledText {
|
||||
gpui::StyledText::new(self.text.clone())
|
||||
.with_highlights(default_style, self.highlights.iter().cloned())
|
||||
.with_default_highlights(default_style, self.highlights.iter().cloned())
|
||||
}
|
||||
|
||||
/// Returns the first line without leading whitespace unless highlighted
|
||||
@@ -964,7 +964,7 @@ impl Buffer {
|
||||
file,
|
||||
capability,
|
||||
syntax_map,
|
||||
parsing_in_background: false,
|
||||
reparse: None,
|
||||
non_text_state_update_count: 0,
|
||||
sync_parse_timeout: Duration::from_millis(1),
|
||||
parse_status: async_watch::channel(ParseStatus::Idle),
|
||||
@@ -1420,7 +1420,7 @@ impl Buffer {
|
||||
/// Whether the buffer is being parsed in the background.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn is_parsing(&self) -> bool {
|
||||
self.parsing_in_background
|
||||
self.reparse.is_some()
|
||||
}
|
||||
|
||||
/// Indicates whether the buffer contains any regions that may be
|
||||
@@ -1458,7 +1458,7 @@ impl Buffer {
|
||||
/// for the same buffer, we only initiate a new parse if we are not already
|
||||
/// parsing in the background.
|
||||
pub fn reparse(&mut self, cx: &mut Context<Self>) {
|
||||
if self.parsing_in_background {
|
||||
if self.reparse.is_some() {
|
||||
return;
|
||||
}
|
||||
let language = if let Some(language) = self.language.clone() {
|
||||
@@ -1492,10 +1492,10 @@ impl Buffer {
|
||||
{
|
||||
Ok(new_syntax_snapshot) => {
|
||||
self.did_finish_parsing(new_syntax_snapshot, cx);
|
||||
self.reparse = None;
|
||||
}
|
||||
Err(parse_task) => {
|
||||
self.parsing_in_background = true;
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
self.reparse = Some(cx.spawn(move |this, mut cx| async move {
|
||||
let new_syntax_map = parse_task.await;
|
||||
this.update(&mut cx, move |this, cx| {
|
||||
let grammar_changed =
|
||||
@@ -1511,14 +1511,13 @@ impl Buffer {
|
||||
|| grammar_changed
|
||||
|| this.version.changed_since(&parsed_version);
|
||||
this.did_finish_parsing(new_syntax_map, cx);
|
||||
this.parsing_in_background = false;
|
||||
this.reparse = None;
|
||||
if parse_again {
|
||||
this.reparse(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use feature_flags::ZedPro;
|
||||
use gpui::{
|
||||
@@ -11,8 +11,8 @@ use language_model::{
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use proto::Plan;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, IconButtonShape, ListItem, ListItemSpacing, PopoverButton,
|
||||
PopoverMenuHandle, Tooltip, TriggerablePopover,
|
||||
prelude::*, ButtonLike, IconButtonShape, ListItem, ListItemSpacing, PopoverMenu,
|
||||
PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use workspace::ShowConfiguration;
|
||||
|
||||
@@ -201,16 +201,6 @@ impl Render for LanguageModelSelector {
|
||||
}
|
||||
}
|
||||
|
||||
impl TriggerablePopover for LanguageModelSelector {
|
||||
fn menu_handle(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) -> PopoverMenuHandle<Self> {
|
||||
self.popover_menu_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
@@ -401,9 +391,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.pl_0p5()
|
||||
.w(px(240.))
|
||||
.child(
|
||||
div().max_w_40().child(
|
||||
Label::new(model_info.model.name().0.clone()).text_ellipsis(),
|
||||
),
|
||||
div()
|
||||
.max_w_40()
|
||||
.child(Label::new(model_info.model.name().0.clone()).truncate()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -493,21 +483,26 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InlineLanguageModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
impl InlineLanguageModelSelector {
|
||||
pub fn new(selector: Entity<LanguageModelSelector>) -> Self {
|
||||
Self { selector }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for InlineLanguageModelSelector {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
PopoverButton::new(
|
||||
self.selector,
|
||||
gpui::Corner::TopRight,
|
||||
pub fn inline_language_model_selector(
|
||||
f: impl Fn(Arc<dyn LanguageModel>, &App) + 'static,
|
||||
) -> AnyElement {
|
||||
let f = Rc::new(f);
|
||||
PopoverMenu::new("popover-button")
|
||||
.menu(move |window, cx| {
|
||||
Some(cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let f = f.clone();
|
||||
move |model, cx| {
|
||||
f(model, cx);
|
||||
}
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -528,36 +523,45 @@ impl RenderOnce for InlineLanguageModelSelector {
|
||||
)
|
||||
},
|
||||
)
|
||||
.render(window, cx)
|
||||
}
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
// .when_some(menu_handle, |el, handle| el.with_handle(handle))
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub struct AssistantLanguageModelSelector {
|
||||
focus_handle: FocusHandle,
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
}
|
||||
pub fn assistant_language_model_selector(
|
||||
keybinding_target: FocusHandle,
|
||||
menu_handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
|
||||
cx: &App,
|
||||
f: impl Fn(Arc<dyn LanguageModel>, &App) + 'static,
|
||||
) -> AnyElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
_ => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
impl AssistantLanguageModelSelector {
|
||||
pub fn new(focus_handle: FocusHandle, selector: Entity<LanguageModelSelector>) -> Self {
|
||||
Self {
|
||||
focus_handle,
|
||||
selector,
|
||||
}
|
||||
}
|
||||
}
|
||||
let f = Rc::new(f);
|
||||
|
||||
impl RenderOnce for AssistantLanguageModelSelector {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
_ => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
PopoverButton::new(
|
||||
self.selector.clone(),
|
||||
Corner::BottomRight,
|
||||
PopoverMenu::new("popover-button")
|
||||
.menu(move |window, cx| {
|
||||
Some(cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let f = f.clone();
|
||||
move |model, cx| {
|
||||
f(model, cx);
|
||||
}
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
@@ -578,12 +582,17 @@ impl RenderOnce for AssistantLanguageModelSelector {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
&keybinding_target,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.render(window, cx)
|
||||
}
|
||||
.anchor(Corner::BottomRight)
|
||||
.when_some(menu_handle, |el, handle| el.with_handle(handle))
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::{borrow::Cow, sync::Arc};
|
||||
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState};
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
@@ -1170,12 +1170,14 @@ impl SearchableItem for LspLogView {
|
||||
}
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
matches: &[Self::Match],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.active_match_index(matches, window, cx))
|
||||
self.editor.update(cx, |e, cx| {
|
||||
e.active_match_index(direction, matches, window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,11 +120,21 @@ impl super::LspAdapter for CLspAdapter {
|
||||
completion: &lsp::CompletionItem,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let label_detail = match &completion.label_details {
|
||||
Some(label_detail) => match &label_detail.detail {
|
||||
Some(detail) => detail.trim(),
|
||||
None => "",
|
||||
},
|
||||
None => "",
|
||||
};
|
||||
|
||||
let label = completion
|
||||
.label
|
||||
.strip_prefix('•')
|
||||
.unwrap_or(&completion.label)
|
||||
.trim();
|
||||
.trim()
|
||||
.to_owned()
|
||||
+ label_detail;
|
||||
|
||||
match completion.kind {
|
||||
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
name: (identifier) @function)
|
||||
(method_definition
|
||||
name: (property_identifier) @function.method)
|
||||
(method_definition
|
||||
name: (property_identifier) @constructor
|
||||
(#eq? @constructor "constructor"))
|
||||
|
||||
(pair
|
||||
key: (property_identifier) @function.method
|
||||
|
||||
@@ -272,6 +272,17 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
|
||||
let node_runtime = node_runtime.clone();
|
||||
move || Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone()))
|
||||
});
|
||||
languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), {
|
||||
let node_runtime = node_runtime.clone();
|
||||
move || Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
});
|
||||
languages.register_available_lsp_adapter(
|
||||
LanguageServerName("typescript-language-server".into()),
|
||||
{
|
||||
let node_runtime = node_runtime.clone();
|
||||
move || Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone()))
|
||||
},
|
||||
);
|
||||
|
||||
// Register Tailwind for the existing languages that should have it by default.
|
||||
//
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
; Identifier naming conventions; these "soft conventions" should stay at the top of the file as they're often overridden
|
||||
(identifier) @variable
|
||||
(attribute attribute: (identifier) @property)
|
||||
|
||||
; CamelCase for classes
|
||||
((identifier) @type.class
|
||||
@@ -8,8 +10,6 @@
|
||||
((identifier) @constant
|
||||
(#match? @constant "^_*[A-Z][A-Z0-9_]*$"))
|
||||
|
||||
(identifier) @variable
|
||||
(attribute attribute: (identifier) @property)
|
||||
(type (identifier) @type)
|
||||
(generic_type (identifier) @type)
|
||||
(comment) @comment
|
||||
@@ -61,6 +61,8 @@
|
||||
(identifier) @function.arguments) ; Typed parameters
|
||||
(default_parameter
|
||||
name: (identifier) @function.arguments) ; Default parameters
|
||||
(typed_default_parameter
|
||||
name: (identifier) @function.arguments) ; Typed default parameters
|
||||
]))
|
||||
|
||||
; Keyword arguments
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
] @string
|
||||
|
||||
(group_name) @property
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"|"
|
||||
"="
|
||||
"!"
|
||||
(any_character)
|
||||
] @operator
|
||||
|
||||
(count_quantifier
|
||||
@@ -44,7 +45,3 @@
|
||||
"^" @operator
|
||||
(class_range "-" @operator)
|
||||
])
|
||||
|
||||
(class_character) @constant.character
|
||||
|
||||
(pattern_character) @string
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
name: (identifier) @function)
|
||||
(method_definition
|
||||
name: (property_identifier) @function.method)
|
||||
(method_definition
|
||||
name: (property_identifier) @constructor
|
||||
(#eq? @constructor "constructor"))
|
||||
|
||||
(pair
|
||||
key: (property_identifier) @function.method
|
||||
@@ -45,9 +48,6 @@
|
||||
|
||||
; Special identifiers
|
||||
|
||||
((identifier) @constructor
|
||||
(#match? @constructor "^[A-Z]"))
|
||||
|
||||
((identifier) @type
|
||||
(#match? @type "^[A-Z]"))
|
||||
(type_identifier) @type
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
name: (identifier) @function)
|
||||
(method_definition
|
||||
name: (property_identifier) @function.method)
|
||||
(method_definition
|
||||
name: (property_identifier) @constructor
|
||||
(#eq? @constructor "constructor"))
|
||||
|
||||
(pair
|
||||
key: (property_identifier) @function.method
|
||||
|
||||
@@ -84,6 +84,7 @@ pub fn main() {
|
||||
selection
|
||||
},
|
||||
heading: Default::default(),
|
||||
..Default::default()
|
||||
};
|
||||
let markdown = cx.new(|cx| {
|
||||
Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, None, cx)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user