Compare commits
58 Commits
miner
...
x11-rework
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb7efff4e | ||
|
|
3956717065 | ||
|
|
d3e2327099 | ||
|
|
fc945cc351 | ||
|
|
da22e0dd0b | ||
|
|
fb3ef0d140 | ||
|
|
e71b642f44 | ||
|
|
6cedfa0ce7 | ||
|
|
209b1d1931 | ||
|
|
6986ac4c27 | ||
|
|
d50d1611b9 | ||
|
|
1260c616ba | ||
|
|
89951f7e66 | ||
|
|
cd81dad2fa | ||
|
|
3a08d7ab43 | ||
|
|
49dc63812a | ||
|
|
c0a3642f77 | ||
|
|
4d5441c09d | ||
|
|
2dc840132b | ||
|
|
5d766f61fa | ||
|
|
18b4573064 | ||
|
|
d044dc8485 | ||
|
|
f00bea5d0f | ||
|
|
b43df6048b | ||
|
|
eb914682b3 | ||
|
|
5b7e31c075 | ||
|
|
922fcaf5a6 | ||
|
|
9f88460870 | ||
|
|
e5d1cf84cf | ||
|
|
41d2c52638 | ||
|
|
d1a55d64a8 | ||
|
|
db06244972 | ||
|
|
597469bbbd | ||
|
|
e0c192d831 | ||
|
|
b2a0a7fa3c | ||
|
|
0b1a589183 | ||
|
|
7e694d1bcf | ||
|
|
890443241d | ||
|
|
b014f9f017 | ||
|
|
f40d2313fb | ||
|
|
2dee4f87fd | ||
|
|
54afa6f69f | ||
|
|
55511d1591 | ||
|
|
6c0cb9eaa3 | ||
|
|
24e7b69f8f | ||
|
|
a4cdca5141 | ||
|
|
86cd87e993 | ||
|
|
88000eb7e2 | ||
|
|
ab5a462e0c | ||
|
|
79430fc7d2 | ||
|
|
f96e4ba84f | ||
|
|
7be1ffb9ec | ||
|
|
93a5d0ca29 | ||
|
|
328d98dddc | ||
|
|
76ab9e4d66 | ||
|
|
c477c12956 | ||
|
|
1ffd87b87e | ||
|
|
df11b646da |
2
.github/workflows/ci.yml
vendored
@@ -254,7 +254,7 @@ jobs:
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
body_file: target/release-notes.md
|
||||
body_path: target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ We plan to set aside time each week to pair program with contributors on promisi
|
||||
|
||||
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
|
||||
|
||||
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation**
|
||||
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.**
|
||||
- [`editor`](/crates/editor) contains the core `Editor` type that drives both the code editor and all various input fields within Zed. It also handles a display layer for LSP features such as Inlay Hints or code completions.
|
||||
- [`project`](/crates/project) manages files and navigation within the filetree. It is also Zed's side of communication with LSP.
|
||||
- [`workspace`](/crates/workspace) handles local state serialization and groups projects together.
|
||||
|
||||
418
Cargo.lock
generated
@@ -34,12 +34,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
@@ -139,6 +133,12 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "aligned-vec"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.16"
|
||||
@@ -284,6 +284,17 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
|
||||
|
||||
[[package]]
|
||||
name = "arg_enum_proc_macro"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.7"
|
||||
@@ -986,6 +997,29 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "av1-grain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"log",
|
||||
"nom",
|
||||
"num-rational",
|
||||
"v_frame",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "avif-serialize"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.1.5"
|
||||
@@ -1432,7 +1466,7 @@ dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide 0.7.1",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
@@ -1555,6 +1589,12 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -1570,6 +1610,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitstream-io"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "415f8399438eb5e4b2f73ed3152a3448b98149dda642a957ee704e1daa5cf1d8"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
@@ -1751,6 +1797,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6a6c0b39c38fd754ac338b00a88066436389c0f029da5d37d1e01091d9b7c17"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.14.0"
|
||||
@@ -1805,6 +1857,12 @@ version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "0.4.12"
|
||||
@@ -2033,6 +2091,16 @@ dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -3238,16 +3306,6 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.8"
|
||||
@@ -3830,6 +3888,22 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.72.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"flume",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extension"
|
||||
version = "0.1.0"
|
||||
@@ -4112,7 +4186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide 0.7.1",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4586,9 +4660,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.11.4"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
|
||||
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
@@ -4845,6 +4919,7 @@ dependencies = [
|
||||
"taffy",
|
||||
"thiserror",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
"usvg",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -5017,6 +5092,12 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "heed"
|
||||
version = "0.20.1"
|
||||
@@ -5349,21 +5430,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.23.14"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
|
||||
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-iter",
|
||||
"num-rational 0.3.2",
|
||||
"image-webp",
|
||||
"num-traits",
|
||||
"png 0.16.8",
|
||||
"scoped_threadpool",
|
||||
"png",
|
||||
"qoi",
|
||||
"ravif",
|
||||
"rayon",
|
||||
"rgb",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5385,6 +5480,12 @@ version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
|
||||
|
||||
[[package]]
|
||||
name = "imgref"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -5498,6 +5599,17 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpolate_name"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-extras"
|
||||
version = "0.18.1"
|
||||
@@ -5699,12 +5811,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.1.22"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
@@ -5953,12 +6062,29 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.17.0+1.8.1"
|
||||
@@ -6151,6 +6277,15 @@ dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loop9"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
|
||||
dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp"
|
||||
version = "0.1.0"
|
||||
@@ -6326,6 +6461,16 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-rayon"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.5"
|
||||
@@ -6433,25 +6578,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
@@ -6681,6 +6807,12 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noop_proc_macro"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "notifications"
|
||||
version = "0.1.0"
|
||||
@@ -6746,7 +6878,7 @@ dependencies = [
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational 0.4.1",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
@@ -6822,6 +6954,17 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-format"
|
||||
version = "0.4.4"
|
||||
@@ -6853,17 +6996,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.1"
|
||||
@@ -6972,7 +7104,7 @@ dependencies = [
|
||||
"jni 0.20.0",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-derive 0.3.3",
|
||||
"num-traits",
|
||||
"oboe-sys",
|
||||
]
|
||||
@@ -7660,18 +7792,6 @@ dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.16.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"deflate",
|
||||
"miniz_oxide 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.13"
|
||||
@@ -7682,7 +7802,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.7.1",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8106,6 +8226,21 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.30.0"
|
||||
@@ -8229,6 +8364,56 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "977b1e897f9d764566891689e642653e5ed90c6895106acd005eb4c1d0203991"
|
||||
|
||||
[[package]]
|
||||
name = "rav1e"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"arg_enum_proc_macro",
|
||||
"arrayvec",
|
||||
"av1-grain",
|
||||
"bitstream-io",
|
||||
"built",
|
||||
"cfg-if",
|
||||
"interpolate_name",
|
||||
"itertools 0.12.1",
|
||||
"libc",
|
||||
"libfuzzer-sys",
|
||||
"log",
|
||||
"maybe-rayon",
|
||||
"new_debug_unreachable",
|
||||
"noop_proc_macro",
|
||||
"num-derive 0.4.2",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"profiling",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"simd_helpers",
|
||||
"system-deps",
|
||||
"thiserror",
|
||||
"v_frame",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ravif"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234"
|
||||
dependencies = [
|
||||
"avif-serialize",
|
||||
"imgref",
|
||||
"loop9",
|
||||
"quick-error",
|
||||
"rav1e",
|
||||
"rayon",
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.5.2"
|
||||
@@ -9028,12 +9213,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scoped_threadpool"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -9560,6 +9739,15 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "simd_helpers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.4"
|
||||
@@ -10368,6 +10556,19 @@ dependencies = [
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.10",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-interface"
|
||||
version = "0.27.1"
|
||||
@@ -10707,12 +10908,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.6.1"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
|
||||
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"jpeg-decoder",
|
||||
"miniz_oxide 0.4.4",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
@@ -10792,7 +10993,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.17.13",
|
||||
"png",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
@@ -11672,6 +11873,17 @@ dependencies = [
|
||||
"sha1_smol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v_frame"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
|
||||
dependencies = [
|
||||
"aligned-vec",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
@@ -11734,6 +11946,12 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
@@ -13332,7 +13550,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.142.0"
|
||||
version = "0.143.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -13751,6 +13969,30 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "4.0.2"
|
||||
|
||||
@@ -308,7 +308,7 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
ignore = "0.4.22"
|
||||
image = "0.23"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "1"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
|
||||
1
assets/icons/book.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-book"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
1
assets/icons/book_copy.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-book-copy"><path d="M2 16V4a2 2 0 0 1 2-2h11"/><path d="M5 14H4a2 2 0 1 0 0 4h1"/><path d="M22 18H11a2 2 0 1 0 0 4h11V6H11a2 2 0 0 0-2 2v12"/></svg>
|
||||
|
After Width: | Height: | Size: 351 B |
1
assets/icons/book_plus.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-book-plus"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/><path d="M9 10h6"/><path d="M12 7v6"/></svg>
|
||||
|
After Width: | Height: | Size: 332 B |
1
assets/icons/chevron_up_down.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-chevrons-up-down"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
1
assets/icons/font.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-type"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/></svg>
|
||||
|
After Width: | Height: | Size: 329 B |
1
assets/icons/font_size.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-a-large-small"><path d="M21 14h-5"/><path d="M16 16v-3.5a2.5 2.5 0 0 1 5 0V16"/><path d="M4.5 13h6"/><path d="m3 16 4.5-9 4.5 9"/></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
1
assets/icons/font_weight.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-bold"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/></svg>
|
||||
|
After Width: | Height: | Size: 296 B |
6
assets/icons/line_height.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 13.6667H12" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 2.33333H12" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 11L8 5L11 11" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 9H10" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 539 B |
1
assets/icons/visible.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-eye"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
@@ -70,6 +70,14 @@
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"a": "project_panel::NewFile",
|
||||
"shift-a": "project_panel::NewDirectory",
|
||||
"f2": "project_panel::Rename",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-d": "project_panel::Duplicate",
|
||||
"cmd-x": "project_panel::Cut",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||
"alt-b": "project_panel::CollapseSelectedEntry",
|
||||
|
||||
@@ -152,7 +152,8 @@
|
||||
// "focus": false
|
||||
// }
|
||||
// ],
|
||||
"ctrl->": "assistant::QuoteSelection"
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -586,8 +587,9 @@
|
||||
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Trash",
|
||||
"delete": "project_panel::Trash",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFinder",
|
||||
|
||||
@@ -188,7 +188,8 @@
|
||||
"focus": false
|
||||
}
|
||||
],
|
||||
"cmd->": "assistant::QuoteSelection"
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -604,6 +605,7 @@
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"cmd-n": "project_panel::NewFile",
|
||||
"cmd-d": "project_panel::Duplicate",
|
||||
"alt-cmd-n": "project_panel::NewDirectory",
|
||||
"cmd-x": "project_panel::Cut",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
@@ -613,8 +615,9 @@
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"bindings": {
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle",
|
||||
"cmd-1": "workspace::ToggleLeftDock",
|
||||
"cmd-6": "diagnostics::Deploy"
|
||||
@@ -94,6 +95,10 @@
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"enter": "project_panel::Open",
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,15 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {}
|
||||
"bindings": {
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-d": "project_panel::Duplicate",
|
||||
"cmd-n": "project_panel::NewFolder",
|
||||
"return": "project_panel::Rename",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"cmd-alt-c": "project_panel::CopyPath"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
|
||||
@@ -688,7 +688,9 @@
|
||||
// "TOML": ["Embargo.lock"]
|
||||
// }
|
||||
//
|
||||
"file_types": {},
|
||||
"file_types": {
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json"]
|
||||
},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
//
|
||||
// If you don't want any of these extensions, add this field to your settings
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
@@ -10,14 +10,14 @@ mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
|
||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
pub(crate) use context_store::*;
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use inline_assistant::*;
|
||||
pub(crate) use model_selector::*;
|
||||
@@ -264,7 +264,7 @@ impl Assistant {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.set_global(Assistant::default());
|
||||
AssistantSettings::register(cx);
|
||||
|
||||
@@ -288,7 +288,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
assistant_slash_command::init(cx);
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
inline_assistant::init(client.telemetry().clone(), cx);
|
||||
inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
|
||||
RustdocStore::init_global(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
@@ -324,6 +324,24 @@ fn register_slash_commands(cx: &mut AppContext) {
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
}
|
||||
|
||||
pub fn humanize_token_count(count: usize) -> String {
|
||||
match count {
|
||||
0..=999 => count.to_string(),
|
||||
1000..=9999 => {
|
||||
let thousands = count / 1000;
|
||||
let hundreds = (count % 1000 + 50) / 100;
|
||||
if hundreds == 0 {
|
||||
format!("{}k", thousands)
|
||||
} else if hundreds == 10 {
|
||||
format!("{}k", thousands + 1)
|
||||
} else {
|
||||
format!("{}.{}k", thousands, hundreds)
|
||||
}
|
||||
}
|
||||
_ => format!("{}k", (count + 500) / 1000),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||
humanize_token_count,
|
||||
prompt_library::open_prompt_library,
|
||||
search::*,
|
||||
slash_command::{
|
||||
@@ -89,6 +90,10 @@ pub fn init(cx: &mut AppContext) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub enum AssistantPanelEvent {
|
||||
ContextEdited,
|
||||
}
|
||||
|
||||
pub struct AssistantPanel {
|
||||
workspace: WeakView<Workspace>,
|
||||
width: Option<Pixels>,
|
||||
@@ -360,11 +365,11 @@ impl AssistantPanel {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let context_editor = assistant
|
||||
let context_editor = assistant_panel
|
||||
.read(cx)
|
||||
.active_context_editor()
|
||||
.and_then(|editor| {
|
||||
@@ -391,25 +396,37 @@ impl AssistantPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
if assistant.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
|
||||
if assistant_panel.update(cx, |panel, cx| panel.is_authenticated(cx)) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(
|
||||
&active_editor,
|
||||
Some(cx.view().downgrade()),
|
||||
include_context,
|
||||
include_context.then_some(&assistant_panel),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
let assistant = assistant.downgrade();
|
||||
let assistant_panel = assistant_panel.downgrade();
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
assistant
|
||||
assistant_panel
|
||||
.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
|
||||
.await?;
|
||||
if assistant.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))? {
|
||||
if assistant_panel
|
||||
.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))?
|
||||
{
|
||||
cx.update(|cx| {
|
||||
let assistant_panel = if include_context {
|
||||
assistant_panel.upgrade()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&active_editor, Some(workspace), include_context, cx)
|
||||
assistant.assist(
|
||||
&active_editor,
|
||||
Some(workspace),
|
||||
assistant_panel.as_ref(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
} else {
|
||||
@@ -460,7 +477,7 @@ impl AssistantPanel {
|
||||
_subscriptions: subscriptions,
|
||||
});
|
||||
self.show_saved_contexts = false;
|
||||
|
||||
cx.emit(AssistantPanelEvent::ContextEdited);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -472,6 +489,7 @@ impl AssistantPanel {
|
||||
) {
|
||||
match event {
|
||||
ContextEditorEvent::TabContentChanged => cx.notify(),
|
||||
ContextEditorEvent::Edited => cx.emit(AssistantPanelEvent::ContextEdited),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,18 +881,33 @@ impl AssistantPanel {
|
||||
context: &Model<Context>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
let remaining_tokens = context.read(cx).remaining_tokens(cx)?;
|
||||
let remaining_tokens_color = if remaining_tokens <= 0 {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
let token_count = context.read(cx).token_count()?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
let remaining_tokens = max_token_count as isize - token_count as isize;
|
||||
let token_count_color = if remaining_tokens <= 0 {
|
||||
Color::Error
|
||||
} else if remaining_tokens <= 500 {
|
||||
} else if token_count as f32 / max_token_count as f32 >= 0.8 {
|
||||
Color::Warning
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
Some(
|
||||
Label::new(remaining_tokens.to_string())
|
||||
.size(LabelSize::Small)
|
||||
.color(remaining_tokens_color),
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(humanize_token_count(token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_count_color),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(max_token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -978,6 +1011,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for AssistantPanel {}
|
||||
impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
|
||||
|
||||
impl FocusableView for AssistantPanel {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
@@ -1538,11 +1572,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
fn remaining_tokens(&self, cx: &AppContext) -> Option<isize> {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
Some(model.max_token_count() as isize - self.token_count? as isize)
|
||||
}
|
||||
|
||||
fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.count_remaining_tokens(cx);
|
||||
}
|
||||
@@ -2183,6 +2212,7 @@ struct PendingCompletion {
|
||||
}
|
||||
|
||||
enum ContextEditorEvent {
|
||||
Edited,
|
||||
TabContentChanged,
|
||||
}
|
||||
|
||||
@@ -2775,6 +2805,7 @@ impl ContextEditor {
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
self.scroll_position = self.cursor_scroll_position(cx);
|
||||
}
|
||||
EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ pub enum AssistantProvider {
|
||||
model: OpenAiModel,
|
||||
api_url: String,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
available_models: Vec<OpenAiModel>,
|
||||
},
|
||||
Anthropic {
|
||||
model: AnthropicModel,
|
||||
@@ -188,6 +189,7 @@ impl Default for AssistantProvider {
|
||||
model: OpenAiModel::default(),
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +204,7 @@ pub enum AssistantProviderContent {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
@@ -272,6 +275,7 @@ impl AssistantSettingsContent {
|
||||
default_model: settings.default_open_ai_model.clone(),
|
||||
api_url: Some(open_ai_api_url.clone()),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
})
|
||||
} else {
|
||||
settings.default_open_ai_model.clone().map(|open_ai_model| {
|
||||
@@ -279,6 +283,7 @@ impl AssistantSettingsContent {
|
||||
default_model: Some(open_ai_model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -326,6 +331,14 @@ impl AssistantSettingsContent {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
Some(AssistantProviderContent::Ollama {
|
||||
default_model: model,
|
||||
..
|
||||
}) => {
|
||||
if let LanguageModel::Ollama(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
provider => match new_model {
|
||||
LanguageModel::Cloud(model) => {
|
||||
*provider = Some(AssistantProviderContent::ZedDotDev {
|
||||
@@ -337,6 +350,7 @@ impl AssistantSettingsContent {
|
||||
default_model: Some(model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
})
|
||||
}
|
||||
LanguageModel::Anthropic(model) => {
|
||||
@@ -481,15 +495,18 @@ impl Settings for AssistantSettings {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
AssistantProviderContent::OpenAi {
|
||||
default_model: model_override,
|
||||
api_url: api_url_override,
|
||||
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
|
||||
available_models: available_models_override,
|
||||
},
|
||||
) => {
|
||||
merge(model, model_override);
|
||||
merge(api_url, api_url_override);
|
||||
merge(available_models, available_models_override);
|
||||
if let Some(low_speed_timeout_in_seconds_override) =
|
||||
low_speed_timeout_in_seconds_override
|
||||
{
|
||||
@@ -550,10 +567,12 @@ impl Settings for AssistantSettings {
|
||||
default_model: model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => AssistantProvider::OpenAi {
|
||||
model: model.unwrap_or_default(),
|
||||
api_url: api_url.unwrap_or_else(|| open_ai::OPEN_AI_API_URL.into()),
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models: available_models.unwrap_or_default(),
|
||||
},
|
||||
AssistantProviderContent::Anthropic {
|
||||
default_model: model,
|
||||
@@ -610,6 +629,7 @@ mod tests {
|
||||
model: OpenAiModel::FourOmni,
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -632,6 +652,7 @@ mod tests {
|
||||
model: OpenAiModel::FourOmni,
|
||||
api_url: "test-url".into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
@@ -652,6 +673,7 @@ mod tests {
|
||||
model: OpenAiModel::Four,
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -24,6 +24,20 @@ use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Choose which model to use for openai provider.
|
||||
/// If the model is not available, try to use the first available model, or fallback to the original model.
|
||||
fn choose_openai_model(
|
||||
model: &::open_ai::Model,
|
||||
available_models: &[::open_ai::Model],
|
||||
) -> ::open_ai::Model {
|
||||
available_models
|
||||
.iter()
|
||||
.find(|&m| m == model)
|
||||
.or_else(|| available_models.first())
|
||||
.unwrap_or_else(|| model)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let mut settings_version = 0;
|
||||
let provider = match &AssistantSettings::get_global(cx).provider {
|
||||
@@ -34,8 +48,9 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
model.clone(),
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
@@ -77,10 +92,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
@@ -136,10 +152,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
model.clone(),
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
@@ -201,10 +218,10 @@ impl CompletionProvider {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> Vec<LanguageModel> {
|
||||
pub fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider
|
||||
.available_models()
|
||||
.available_models(cx)
|
||||
.map(LanguageModel::OpenAi)
|
||||
.collect(),
|
||||
CompletionProvider::Anthropic(provider) => provider
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::assistant_settings::CloudModel;
|
||||
use crate::assistant_settings::{AssistantProvider, AssistantSettings};
|
||||
use crate::{
|
||||
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
@@ -56,8 +57,26 @@ impl OpenAiCompletionProvider {
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = OpenAiModel> {
|
||||
OpenAiModel::iter()
|
||||
pub fn available_models(&self, cx: &AppContext) -> impl Iterator<Item = OpenAiModel> {
|
||||
if let AssistantProvider::OpenAi {
|
||||
available_models, ..
|
||||
} = &AssistantSettings::get_global(cx).provider
|
||||
{
|
||||
if !available_models.is_empty() {
|
||||
// available_models is set, just return it
|
||||
return available_models.clone().into_iter();
|
||||
}
|
||||
}
|
||||
let available_models = if matches!(self.model, OpenAiModel::Custom { .. }) {
|
||||
// available_models is not set but the default model is set to custom, only show custom
|
||||
vec![self.model.clone()]
|
||||
} else {
|
||||
// default case, use all models except custom
|
||||
OpenAiModel::iter()
|
||||
.filter(|model| !matches!(model, OpenAiModel::Custom { .. }))
|
||||
.collect()
|
||||
};
|
||||
available_models.into_iter()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
@@ -213,7 +232,8 @@ pub fn count_open_ai_tokens(
|
||||
| LanguageModel::Cloud(CloudModel::Claude3_5Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Opus)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku)
|
||||
| LanguageModel::OpenAi(OpenAiModel::Custom { .. }) => {
|
||||
// Tiktoken doesn't yet support these models, so we manually use the
|
||||
// same tokenizer as GPT-4.
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
prompts::generate_content_prompt, AssistantPanel, CompletionProvider, Hunk,
|
||||
LanguageModelRequest, LanguageModelRequestMessage, Role, StreamingDiff,
|
||||
assistant_settings::AssistantSettings, humanize_token_count, prompts::generate_content_prompt,
|
||||
AssistantPanel, AssistantPanelEvent, CompletionProvider, Hunk, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role, StreamingDiff,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use editor::{
|
||||
@@ -14,6 +15,7 @@ use editor::{
|
||||
Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
|
||||
ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{
|
||||
point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
|
||||
@@ -24,7 +26,7 @@ use language::{Buffer, Point, Selection, TransactionId};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use settings::Settings;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use similar::TextDiff;
|
||||
use std::{
|
||||
cmp, mem,
|
||||
@@ -32,15 +34,15 @@ use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{self, Poll},
|
||||
time::Instant,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
use util::RangeExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
pub fn init(telemetry: Arc<Telemetry>, cx: &mut AppContext) {
|
||||
cx.set_global(InlineAssistant::new(telemetry));
|
||||
pub fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
|
||||
cx.set_global(InlineAssistant::new(fs, telemetry));
|
||||
}
|
||||
|
||||
const PROMPT_HISTORY_MAX_LEN: usize = 20;
|
||||
@@ -53,12 +55,13 @@ pub struct InlineAssistant {
|
||||
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
|
||||
prompt_history: VecDeque<String>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
|
||||
impl InlineAssistant {
|
||||
pub fn new(telemetry: Arc<Telemetry>) -> Self {
|
||||
pub fn new(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>) -> Self {
|
||||
Self {
|
||||
next_assist_id: InlineAssistId::default(),
|
||||
next_assist_group_id: InlineAssistGroupId::default(),
|
||||
@@ -67,6 +70,7 @@ impl InlineAssistant {
|
||||
assist_groups: HashMap::default(),
|
||||
prompt_history: VecDeque::default(),
|
||||
telemetry: Some(telemetry),
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +78,7 @@ impl InlineAssistant {
|
||||
&mut self,
|
||||
editor: &View<Editor>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
include_context: bool,
|
||||
assistant_panel: Option<&View<AssistantPanel>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
@@ -151,7 +155,10 @@ impl InlineAssistant {
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
codegen.clone(),
|
||||
editor,
|
||||
assistant_panel,
|
||||
workspace.clone(),
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -208,7 +215,7 @@ impl InlineAssistant {
|
||||
InlineAssist::new(
|
||||
assist_id,
|
||||
assist_group_id,
|
||||
include_context,
|
||||
assistant_panel.is_some(),
|
||||
editor,
|
||||
&prompt_editor,
|
||||
block_ids[0],
|
||||
@@ -706,8 +713,6 @@ impl InlineAssistant {
|
||||
return;
|
||||
}
|
||||
|
||||
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
|
||||
|
||||
let Some(user_prompt) = assist
|
||||
.decorations
|
||||
.as_ref()
|
||||
@@ -716,115 +721,138 @@ impl InlineAssistant {
|
||||
return;
|
||||
};
|
||||
|
||||
let context = if assist.include_context {
|
||||
assist.workspace.as_ref().and_then(|workspace| {
|
||||
let workspace = workspace.upgrade()?.read(cx);
|
||||
let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
|
||||
assistant_panel.read(cx).active_context(cx)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let editor = if let Some(editor) = assist.editor.upgrade() {
|
||||
editor
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project_name = assist.workspace.as_ref().and_then(|workspace| {
|
||||
let workspace = workspace.upgrade()?;
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktree_root_names(cx)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("/"),
|
||||
)
|
||||
});
|
||||
|
||||
self.prompt_history.retain(|prompt| *prompt != user_prompt);
|
||||
self.prompt_history.push_back(user_prompt.clone());
|
||||
if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
|
||||
self.prompt_history.pop_front();
|
||||
}
|
||||
|
||||
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
|
||||
let codegen = assist.codegen.clone();
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let range = codegen.read(cx).range.clone();
|
||||
let start = snapshot.point_to_buffer_offset(range.start);
|
||||
let end = snapshot.point_to_buffer_offset(range.end);
|
||||
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
|
||||
let (start_buffer, start_buffer_offset) = start;
|
||||
let (end_buffer, end_buffer_offset) = end;
|
||||
if start_buffer.remote_id() == end_buffer.remote_id() {
|
||||
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
|
||||
} else {
|
||||
self.finish_assist(assist_id, false, cx);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
self.finish_assist(assist_id, false, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let language = buffer.language_at(range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
|
||||
None
|
||||
} else {
|
||||
Some(language.name())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Higher Temperature increases the randomness of model outputs.
|
||||
// If Markdown or No Language is Known, increase the randomness for more creative output
|
||||
// If Code, decrease temperature to get more deterministic outputs
|
||||
let temperature = if let Some(language) = language_name.clone() {
|
||||
if language.as_ref() == "Markdown" {
|
||||
1.0
|
||||
} else {
|
||||
0.5
|
||||
}
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let prompt = cx.background_executor().spawn(async move {
|
||||
let language_name = language_name.as_deref();
|
||||
generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
|
||||
});
|
||||
|
||||
let mut messages = Vec::new();
|
||||
if let Some(context) = context {
|
||||
let request = context.read(cx).to_completion_request(cx);
|
||||
messages = request.messages;
|
||||
}
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
let request = self.request_for_inline_assist(assist_id, cx);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let prompt = prompt.await?;
|
||||
let request = request.await?;
|
||||
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn request_for_inline_assist(
|
||||
&self,
|
||||
assist_id: InlineAssistId,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<LanguageModelRequest>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (user_prompt, context_request, project_name, buffer, range, model) = cx
|
||||
.read_global(|this: &InlineAssistant, cx: &WindowContext| {
|
||||
let assist = this.assists.get(&assist_id).context("invalid assist")?;
|
||||
let decorations = assist.decorations.as_ref().context("invalid assist")?;
|
||||
let editor = assist.editor.upgrade().context("invalid assist")?;
|
||||
let user_prompt = decorations.prompt_editor.read(cx).prompt(cx);
|
||||
let context_request = if assist.include_context {
|
||||
assist.workspace.as_ref().and_then(|workspace| {
|
||||
let workspace = workspace.upgrade()?.read(cx);
|
||||
let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
|
||||
Some(
|
||||
assistant_panel
|
||||
.read(cx)
|
||||
.active_context(cx)?
|
||||
.read(cx)
|
||||
.to_completion_request(cx),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let project_name = assist.workspace.as_ref().and_then(|workspace| {
|
||||
let workspace = workspace.upgrade()?;
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktree_root_names(cx)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("/"),
|
||||
)
|
||||
});
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let range = assist.codegen.read(cx).range.clone();
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
anyhow::Ok((
|
||||
user_prompt,
|
||||
context_request,
|
||||
project_name,
|
||||
buffer,
|
||||
range,
|
||||
model,
|
||||
))
|
||||
})??;
|
||||
|
||||
let language = buffer.language_at(range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
|
||||
None
|
||||
} else {
|
||||
Some(language.name())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Higher Temperature increases the randomness of model outputs.
|
||||
// If Markdown or No Language is Known, increase the randomness for more creative output
|
||||
// If Code, decrease temperature to get more deterministic outputs
|
||||
let temperature = if let Some(language) = language_name.clone() {
|
||||
if language.as_ref() == "Markdown" {
|
||||
1.0
|
||||
} else {
|
||||
0.5
|
||||
}
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let prompt = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let language_name = language_name.as_deref();
|
||||
let start = buffer.point_to_buffer_offset(range.start);
|
||||
let end = buffer.point_to_buffer_offset(range.end);
|
||||
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
|
||||
let (start_buffer, start_buffer_offset) = start;
|
||||
let (end_buffer, end_buffer_offset) = end;
|
||||
if start_buffer.remote_id() == end_buffer.remote_id() {
|
||||
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
|
||||
} else {
|
||||
return Err(anyhow!("invalid transformation range"));
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("invalid transformation range"));
|
||||
};
|
||||
generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::new();
|
||||
if let Some(context_request) = context_request {
|
||||
messages = context_request.messages;
|
||||
}
|
||||
|
||||
messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: prompt,
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
Ok(LanguageModelRequest {
|
||||
model,
|
||||
messages,
|
||||
stop: vec!["|END|>".to_string()],
|
||||
temperature,
|
||||
};
|
||||
|
||||
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||
@@ -1142,6 +1170,7 @@ enum PromptEditorEvent {
|
||||
|
||||
struct PromptEditor {
|
||||
id: InlineAssistId,
|
||||
fs: Arc<dyn Fs>,
|
||||
height_in_lines: u8,
|
||||
editor: View<Editor>,
|
||||
edited_since_done: bool,
|
||||
@@ -1150,9 +1179,12 @@ struct PromptEditor {
|
||||
prompt_history_ix: Option<usize>,
|
||||
pending_prompt: String,
|
||||
codegen: Model<Codegen>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
_codegen_subscription: Subscription,
|
||||
editor_subscriptions: Vec<Subscription>,
|
||||
pending_token_count: Task<Result<()>>,
|
||||
token_count: Option<usize>,
|
||||
_token_count_subscriptions: Vec<Subscription>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
@@ -1160,6 +1192,7 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let gutter_dimensions = *self.gutter_dimensions.lock();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
let buttons = match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
@@ -1245,85 +1278,101 @@ impl Render for PromptEditor {
|
||||
}
|
||||
};
|
||||
|
||||
v_flex().h_full().w_full().justify_end().child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.py_1p5()
|
||||
.w_full()
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::move_down))
|
||||
.child(
|
||||
h_flex()
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
// .pr(gutter_dimensions.fold_area_width())
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.children(self.workspace.clone().map(|workspace| {
|
||||
IconButton::new("context", IconName::Context)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click({
|
||||
let workspace = workspace.clone();
|
||||
cx.listener(move |_, _, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AssistantPanel>(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.py_1p5()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::move_down))
|
||||
.child(
|
||||
h_flex()
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::global(cx).available_models(cx)
|
||||
{
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
move |_| {
|
||||
Label::new(model.display_name())
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
let model = model.clone();
|
||||
move |cx| {
|
||||
let model = model.clone();
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings| settings.set_model(model),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
let token_count = workspace.upgrade().and_then(|workspace| {
|
||||
let panel =
|
||||
workspace.read(cx).panel::<AssistantPanel>(cx)?;
|
||||
let context = panel.read(cx).active_context(cx)?;
|
||||
context.read(cx).token_count()
|
||||
});
|
||||
if let Some(token_count) = token_count {
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"{} Additional Context Tokens from Assistant",
|
||||
token_count
|
||||
"Using {}",
|
||||
CompletionProvider::global(cx)
|
||||
.model()
|
||||
.display_name()
|
||||
),
|
||||
Some(&crate::ToggleFocus),
|
||||
"Click to open…",
|
||||
None,
|
||||
"Click to Change Model",
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::for_action(
|
||||
"Toggle Assistant Panel",
|
||||
&crate::ToggleFocus,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
}))
|
||||
.children(
|
||||
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
Some(
|
||||
div()
|
||||
.id("error")
|
||||
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
|
||||
.child(
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(div().flex_1().child(self.render_prompt_editor(cx)))
|
||||
.child(h_flex().gap_2().pr_4().children(buttons)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomRight),
|
||||
)
|
||||
.children(
|
||||
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
Some(
|
||||
div()
|
||||
.id("error")
|
||||
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
|
||||
.child(
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(div().flex_1().child(self.render_prompt_editor(cx)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.pr_4()
|
||||
.children(self.render_token_count(cx))
|
||||
.children(buttons),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1336,13 +1385,17 @@ impl FocusableView for PromptEditor {
|
||||
impl PromptEditor {
|
||||
const MAX_LINES: u8 = 8;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: InlineAssistId,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_buffer: Model<MultiBuffer>,
|
||||
codegen: Model<Codegen>,
|
||||
parent_editor: &View<Editor>,
|
||||
assistant_panel: Option<&View<AssistantPanel>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
@@ -1363,6 +1416,15 @@ impl PromptEditor {
|
||||
editor.set_placeholder_text("Add a prompt…", cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let mut token_count_subscriptions = Vec::new();
|
||||
token_count_subscriptions
|
||||
.push(cx.subscribe(parent_editor, Self::handle_parent_editor_event));
|
||||
if let Some(assistant_panel) = assistant_panel {
|
||||
token_count_subscriptions
|
||||
.push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event));
|
||||
}
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
height_in_lines: 1,
|
||||
@@ -1375,9 +1437,14 @@ impl PromptEditor {
|
||||
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
editor_subscriptions: Vec::new(),
|
||||
codegen,
|
||||
fs,
|
||||
pending_token_count: Task::ready(Ok(())),
|
||||
token_count: None,
|
||||
_token_count_subscriptions: token_count_subscriptions,
|
||||
workspace,
|
||||
};
|
||||
this.count_lines(cx);
|
||||
this.count_tokens(cx);
|
||||
this.subscribe_to_editor(cx);
|
||||
this
|
||||
}
|
||||
@@ -1436,6 +1503,47 @@ impl PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_parent_editor_event(
|
||||
&mut self,
|
||||
_: View<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let EditorEvent::BufferEdited { .. } = event {
|
||||
self.count_tokens(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_assistant_panel_event(
|
||||
&mut self,
|
||||
_: View<AssistantPanel>,
|
||||
event: &AssistantPanelEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let AssistantPanelEvent::ContextEdited { .. } = event;
|
||||
self.count_tokens(cx);
|
||||
}
|
||||
|
||||
fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let assist_id = self.id;
|
||||
self.pending_token_count = cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||
let request = cx
|
||||
.update_global(|inline_assistant: &mut InlineAssistant, cx| {
|
||||
inline_assistant.request_for_inline_assist(assist_id, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let token_count = cx
|
||||
.update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
self.count_lines(cx);
|
||||
}
|
||||
@@ -1460,6 +1568,9 @@ impl PromptEditor {
|
||||
self.edited_since_done = true;
|
||||
cx.notify();
|
||||
}
|
||||
EditorEvent::BufferEdited => {
|
||||
self.count_tokens(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1551,6 +1662,63 @@ impl PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
let token_count = self.token_count?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
let remaining_tokens = max_token_count as isize - token_count as isize;
|
||||
let token_count_color = if remaining_tokens <= 0 {
|
||||
Color::Error
|
||||
} else if token_count as f32 / max_token_count as f32 >= 0.8 {
|
||||
Color::Warning
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let mut token_count = h_flex()
|
||||
.id("token_count")
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(humanize_token_count(token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_count_color),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(max_token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
);
|
||||
if let Some(workspace) = self.workspace.clone() {
|
||||
token_count = token_count
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Tokens Used by Inline Assistant",
|
||||
None,
|
||||
"Click to Open Assistant Panel",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.on_click(move |_, cx| {
|
||||
cx.stop_propagation();
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AssistantPanel>(cx)
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
} else {
|
||||
token_count = token_count
|
||||
.cursor_default()
|
||||
.tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
|
||||
}
|
||||
|
||||
Some(token_count)
|
||||
}
|
||||
|
||||
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
@@ -1893,6 +2061,11 @@ impl Codegen {
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
if line_indent.is_none() {
|
||||
// Don't write out the leading indentation in empty lines on the next line
|
||||
// This is the case where the above if statement didn't clear the buffer
|
||||
new_text.clear();
|
||||
}
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ impl RenderOnce for ModelSelector {
|
||||
.with_handle(self.handle)
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::global(cx).available_models() {
|
||||
for model in CompletionProvider::global(cx).available_models(cx) {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
|
||||
@@ -6,16 +6,16 @@ use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorEvent};
|
||||
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
use futures::{
|
||||
future::{self, BoxFuture, Shared},
|
||||
FutureExt,
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, percentage, point, size, Animation, AnimationExt, AppContext, BackgroundExecutor,
|
||||
Bounds, EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
|
||||
Transformation, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter,
|
||||
Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
|
||||
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
||||
@@ -109,12 +109,13 @@ pub struct PromptLibrary {
|
||||
}
|
||||
|
||||
struct PromptEditor {
|
||||
editor: View<Editor>,
|
||||
title_editor: View<Editor>,
|
||||
body_editor: View<Editor>,
|
||||
token_count: Option<usize>,
|
||||
pending_token_count: Task<Option<()>>,
|
||||
next_body_to_save: Option<Rope>,
|
||||
next_title_and_body_to_save: Option<(String, Rope)>,
|
||||
pending_save: Option<Task<Option<()>>>,
|
||||
_subscription: Subscription,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct PromptPickerDelegate {
|
||||
@@ -345,7 +346,8 @@ impl PromptLibrary {
|
||||
|
||||
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
|
||||
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
let body = prompt_editor.editor.update(cx, |editor, cx| {
|
||||
let title = prompt_editor.title_editor.read(cx).text(cx);
|
||||
let body = prompt_editor.body_editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
@@ -359,20 +361,24 @@ impl PromptLibrary {
|
||||
let store = self.store.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
|
||||
prompt_editor.next_body_to_save = Some(body);
|
||||
prompt_editor.next_title_and_body_to_save = Some((title, body));
|
||||
if prompt_editor.pending_save.is_none() {
|
||||
prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
loop {
|
||||
let next_body_to_save = this.update(&mut cx, |this, _| {
|
||||
let title_and_body = this.update(&mut cx, |this, _| {
|
||||
this.prompt_editors
|
||||
.get_mut(&prompt_id)?
|
||||
.next_body_to_save
|
||||
.next_title_and_body_to_save
|
||||
.take()
|
||||
})?;
|
||||
|
||||
if let Some(body) = next_body_to_save {
|
||||
let title = title_from_body(body.chars_at(0));
|
||||
if let Some((title, body)) = title_and_body {
|
||||
let title = if title.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::from(title))
|
||||
};
|
||||
store
|
||||
.save(prompt_id, title, prompt_metadata.default, body)
|
||||
.await
|
||||
@@ -425,11 +431,11 @@ impl PromptLibrary {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
if focus {
|
||||
prompt_editor
|
||||
.editor
|
||||
.body_editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
}
|
||||
self.set_active_prompt(Some(prompt_id), cx);
|
||||
} else {
|
||||
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
let prompt = self.store.load(prompt_id);
|
||||
@@ -438,13 +444,20 @@ impl PromptLibrary {
|
||||
let markdown = language_registry.language_for_name("Markdown").await;
|
||||
this.update(&mut cx, |this, cx| match prompt {
|
||||
Ok(prompt) => {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(prompt, cx);
|
||||
buffer.set_language(markdown.log_err(), cx);
|
||||
buffer.set_language_registry(language_registry);
|
||||
buffer
|
||||
let title_editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_width(cx);
|
||||
editor.set_placeholder_text("Untitled", cx);
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
|
||||
editor
|
||||
});
|
||||
let editor = cx.new_view(|cx| {
|
||||
let body_editor = cx.new_view(|cx| {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(prompt, cx);
|
||||
buffer.set_language(markdown.log_err(), cx);
|
||||
buffer.set_language_registry(language_registry);
|
||||
buffer
|
||||
});
|
||||
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
@@ -460,19 +473,24 @@ impl PromptLibrary {
|
||||
}
|
||||
editor
|
||||
});
|
||||
let _subscription =
|
||||
cx.subscribe(&editor, move |this, _editor, event, cx| {
|
||||
this.handle_prompt_editor_event(prompt_id, event, cx)
|
||||
});
|
||||
let _subscriptions = vec![
|
||||
cx.subscribe(&title_editor, move |this, editor, event, cx| {
|
||||
this.handle_prompt_title_editor_event(prompt_id, editor, event, cx)
|
||||
}),
|
||||
cx.subscribe(&body_editor, move |this, editor, event, cx| {
|
||||
this.handle_prompt_body_editor_event(prompt_id, editor, event, cx)
|
||||
}),
|
||||
];
|
||||
this.prompt_editors.insert(
|
||||
prompt_id,
|
||||
PromptEditor {
|
||||
editor,
|
||||
next_body_to_save: None,
|
||||
title_editor,
|
||||
body_editor,
|
||||
next_title_and_body_to_save: None,
|
||||
pending_save: None,
|
||||
token_count: None,
|
||||
pending_token_count: Task::ready(None),
|
||||
_subscription,
|
||||
_subscriptions,
|
||||
},
|
||||
);
|
||||
this.set_active_prompt(Some(prompt_id), cx);
|
||||
@@ -549,7 +567,7 @@ impl PromptLibrary {
|
||||
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_prompt) = self.active_prompt_id {
|
||||
self.prompt_editors[&active_prompt]
|
||||
.editor
|
||||
.body_editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
cx.stop_propagation();
|
||||
}
|
||||
@@ -565,11 +583,11 @@ impl PromptLibrary {
|
||||
return;
|
||||
};
|
||||
|
||||
let prompt_editor = &self.prompt_editors[&active_prompt_id].editor;
|
||||
let prompt_editor = &self.prompt_editors[&active_prompt_id].body_editor;
|
||||
let provider = CompletionProvider::global(cx);
|
||||
if provider.is_authenticated() {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, false, cx)
|
||||
assistant.assist(&prompt_editor, None, None, cx)
|
||||
})
|
||||
} else {
|
||||
for window in cx.windows() {
|
||||
@@ -589,50 +607,73 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_event(
|
||||
fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext<Self>) {
|
||||
if let Some(prompt_id) = self.active_prompt_id {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
cx.focus_view(&prompt_editor.body_editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
|
||||
if let Some(prompt_id) = self.active_prompt_id {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
cx.focus_view(&prompt_editor.title_editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_prompt_title_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
title_editor: View<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let EditorEvent::BufferEdited = event {
|
||||
let prompt_editor = self.prompt_editors.get(&prompt_id).unwrap();
|
||||
let buffer = prompt_editor
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap();
|
||||
match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
title_editor.update(cx, |title_editor, cx| {
|
||||
title_editor.change_selections(None, cx, |selections| {
|
||||
let cursor = selections.oldest_anchor().head();
|
||||
selections.select_anchor_ranges([cursor..cursor]);
|
||||
});
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let mut chars = buffer.chars_at(0);
|
||||
match chars.next() {
|
||||
Some('#') => {
|
||||
if chars.next() != Some(' ') {
|
||||
drop(chars);
|
||||
buffer.edit([(1..1, " ")], None, cx);
|
||||
}
|
||||
}
|
||||
Some(' ') => {
|
||||
drop(chars);
|
||||
buffer.edit([(0..0, "#")], None, cx);
|
||||
}
|
||||
_ => {
|
||||
drop(chars);
|
||||
buffer.edit([(0..0, "# ")], None, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
fn handle_prompt_body_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
body_editor: View<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
body_editor.update(cx, |body_editor, cx| {
|
||||
body_editor.change_selections(None, cx, |selections| {
|
||||
let cursor = selections.oldest_anchor().head();
|
||||
selections.select_anchor_ranges([cursor..cursor]);
|
||||
});
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(prompt) = self.prompt_editors.get_mut(&prompt_id) {
|
||||
let editor = &prompt.editor.read(cx);
|
||||
let editor = &prompt.body_editor.read(cx);
|
||||
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
let body = buffer.as_rope().clone();
|
||||
prompt.pending_token_count = cx.spawn(|this, mut cx| {
|
||||
@@ -708,122 +749,214 @@ impl PromptLibrary {
|
||||
.flex_none()
|
||||
.min_w_64()
|
||||
.children(self.active_prompt_id.and_then(|prompt_id| {
|
||||
let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
|
||||
let prompt_metadata = self.store.metadata(prompt_id)?;
|
||||
let prompt_editor = &self.prompt_editors[&prompt_id];
|
||||
let focus_handle = prompt_editor.editor.focus_handle(cx);
|
||||
let focus_handle = prompt_editor.body_editor.focus_handle(cx);
|
||||
let current_model = CompletionProvider::global(cx).model();
|
||||
let token_count = prompt_editor.token_count.map(|count| count.to_string());
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
v_flex()
|
||||
.id("prompt-editor-inner")
|
||||
.size_full()
|
||||
.items_start()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.pl(Spacing::XXLarge.rems(cx))
|
||||
.pt(Spacing::Large.rems(cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.focus(&focus_handle);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.on_action(cx.listener(Self::focus_picker))
|
||||
.on_action(cx.listener(Self::inline_assist))
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.pt(Spacing::XXLarge.rems(cx))
|
||||
.pl(Spacing::XXLarge.rems(cx))
|
||||
.child(prompt_editor.editor.clone()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_12()
|
||||
.py(Spacing::Large.rems(cx))
|
||||
.justify_start()
|
||||
.items_end()
|
||||
.gap_1()
|
||||
.child(h_flex().h_8().font_family(buffer_font).when_some_else(
|
||||
token_count,
|
||||
|tokens_ready, token_count| {
|
||||
tokens_ready.pr_3().justify_end().child(
|
||||
// This isn't actually a button, it just let's us easily add
|
||||
// a tooltip to the token count.
|
||||
Button::new("token_count", token_count.clone())
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!("{} tokens", token_count,),
|
||||
None,
|
||||
format!(
|
||||
"Model: {}",
|
||||
current_model.display_name()
|
||||
),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
|tokens_loading| {
|
||||
tokens_loading.w_12().justify_center().child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
))
|
||||
h_flex()
|
||||
.group("active-editor-header")
|
||||
.pr(Spacing::XXLarge.rems(cx))
|
||||
.pt(Spacing::XSmall.rems(cx))
|
||||
.pb(Spacing::Large.rems(cx))
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex().justify_center().w_12().h_8().child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.selected(prompt_metadata.default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if prompt_metadata.default {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
h_flex().gap_1().child(
|
||||
div()
|
||||
.max_w_80()
|
||||
.on_action(cx.listener(Self::move_down_from_title))
|
||||
.border_1()
|
||||
.border_color(transparent_black())
|
||||
.rounded_md()
|
||||
.group_hover("active-editor-header", |this| {
|
||||
this.border_color(
|
||||
cx.theme().colors().border_variant,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(ToggleDefaultPrompt));
|
||||
}),
|
||||
.child(EditorElement::new(
|
||||
&prompt_editor.title_editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().system().transparent,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: TextStyle {
|
||||
color: cx
|
||||
.theme()
|
||||
.colors()
|
||||
.editor_foreground,
|
||||
font_family: settings
|
||||
.ui_font
|
||||
.family
|
||||
.clone(),
|
||||
font_features: settings
|
||||
.ui_font
|
||||
.features
|
||||
.clone(),
|
||||
font_size: HeadlineSize::Large
|
||||
.size()
|
||||
.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(
|
||||
settings.buffer_line_height.value(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
scrollbar_width: Pixels::ZERO,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
status: cx.theme().status().clone(),
|
||||
inlay_hints_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().hint),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
suggestions_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex().justify_center().w_12().h_8().child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.size(ButtonSize::Large)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt",
|
||||
&DeletePrompt,
|
||||
cx,
|
||||
h_flex()
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap(Spacing::XXLarge.rems(cx))
|
||||
.child(div()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap(Spacing::XXLarge.rems(cx))
|
||||
.children(prompt_editor.token_count.map(
|
||||
|token_count| {
|
||||
let token_count: SharedString =
|
||||
token_count.to_string().into();
|
||||
let label_token_count: SharedString =
|
||||
token_count.to_string().into();
|
||||
|
||||
h_flex()
|
||||
.id("token_count")
|
||||
.tooltip(move |cx| {
|
||||
let token_count =
|
||||
token_count.clone();
|
||||
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"{} tokens",
|
||||
token_count.clone()
|
||||
),
|
||||
None,
|
||||
format!(
|
||||
"Model: {}",
|
||||
current_model
|
||||
.display_name()
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} tokens",
|
||||
label_token_count.clone()
|
||||
))
|
||||
.color(Color::Muted),
|
||||
)
|
||||
},
|
||||
))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"delete-prompt",
|
||||
IconName::Trash,
|
||||
)
|
||||
.size(ButtonSize::Large)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt",
|
||||
&DeletePrompt,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
}),
|
||||
),
|
||||
// .child(
|
||||
// IconButton::new(
|
||||
// "duplicate-prompt",
|
||||
// IconName::BookCopy,
|
||||
// )
|
||||
// .size(ButtonSize::Large)
|
||||
// .style(ButtonStyle::Transparent)
|
||||
// .shape(IconButtonShape::Square)
|
||||
// .size(ButtonSize::Large)
|
||||
// .tooltip(move |cx| {
|
||||
// Tooltip::for_action(
|
||||
// "Duplicate Prompt",
|
||||
// &gpui::NoAction,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .disabled(true),
|
||||
// )
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
IconName::Sparkle,
|
||||
)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.selected(prompt_metadata.default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if prompt_metadata.default {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
ToggleDefaultPrompt,
|
||||
));
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.on_action(cx.listener(Self::focus_picker))
|
||||
.on_action(cx.listener(Self::inline_assist))
|
||||
.on_action(cx.listener(Self::move_up_from_body))
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.child(prompt_editor.body_editor.clone()),
|
||||
),
|
||||
)
|
||||
}))
|
||||
@@ -1115,24 +1248,3 @@ pub struct GlobalPromptStore(
|
||||
);
|
||||
|
||||
impl Global for GlobalPromptStore {}
|
||||
|
||||
fn title_from_body(body: impl IntoIterator<Item = char>) -> Option<SharedString> {
|
||||
let mut chars = body.into_iter().take_while(|c| *c != '\n').peekable();
|
||||
|
||||
let mut level = 0;
|
||||
while let Some('#') = chars.peek() {
|
||||
level += 1;
|
||||
chars.next();
|
||||
}
|
||||
|
||||
if level > 0 {
|
||||
let title = chars.collect::<String>().trim().to_string();
|
||||
if title.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(title.into())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,118 +6,106 @@ pub fn generate_content_prompt(
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
_project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
"Here's a file of text that I'm going to ask you to make an edit to."
|
||||
)?;
|
||||
"Code"
|
||||
"text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Here's a file of {language_name} that I'm going to ask you to make an edit to."
|
||||
)?;
|
||||
"code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"The user has the following file open in the editor:"
|
||||
)?;
|
||||
const MAX_CTX: usize = 50000;
|
||||
let mut is_truncated = false;
|
||||
if range.is_empty() {
|
||||
write!(prompt, "```")?;
|
||||
if let Some(language_name) = language_name {
|
||||
write!(prompt, "{language_name}")?;
|
||||
}
|
||||
|
||||
for chunk in buffer.as_rope().chunks_in_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
prompt.push_str("<|CURSOR|>");
|
||||
for chunk in buffer.as_rope().chunks_in_range(range.start..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|CURSOR|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
prompt.push_str("The point you'll need to insert at is marked with <insert_here></insert_here>.\n\n<document>");
|
||||
} else {
|
||||
write!(prompt, "```")?;
|
||||
for chunk in buffer.as_rope().chunks() {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"In particular, the following piece of text is selected:"
|
||||
)?;
|
||||
write!(prompt, "```")?;
|
||||
if let Some(language_name) = language_name {
|
||||
write!(prompt, "{language_name}")?;
|
||||
}
|
||||
prompt.push('\n');
|
||||
prompt.push_str("The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.\n\n<document>");
|
||||
}
|
||||
// Include file content.
|
||||
let before_range = 0..range.start;
|
||||
let truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.start - MAX_CTX..range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
let mut non_rewrite_len = truncated_before.len();
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"Modify the user's selected {content_type} based upon the users prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"You must reply with only the adjusted {content_type}, not the entire file."
|
||||
)
|
||||
.unwrap();
|
||||
prompt.push_str("\n<rewrite_this>");
|
||||
} else {
|
||||
prompt.push_str("<insert_here></insert_here>");
|
||||
}
|
||||
let after_range = range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.end..range.end + MAX_CTX
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
non_rewrite_len += truncated_after.len();
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
write!(prompt, "</document>\n\n").unwrap();
|
||||
|
||||
if is_truncated {
|
||||
writeln!(prompt, "The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.\n")?;
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You can't replace {content_type}, your answer will be inserted in place of the `<insert_here></insert_here>` tags. Don't include the insert_here tags in your output.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the following prompt:\n\n<prompt>\n{user_prompt}\n</prompt>",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(prompt, "Match the indentation in the original file in the inserted {content_type}, don't include any indentation on blank lines.\n").unwrap();
|
||||
prompt.push_str("Immediately start with the following format with no remarks:\n\n```\n{{INSERTED_CODE}}\n```");
|
||||
} else {
|
||||
writeln!(prompt, "Edit the section of {content_type} in <rewrite_this></rewrite_this> tags based on the following prompt:'").unwrap();
|
||||
writeln!(prompt, "\n<prompt>\n{user_prompt}\n</prompt>\n").unwrap();
|
||||
let rewrite_len = range.end - range.start;
|
||||
if rewrite_len < 20000 && rewrite_len * 2 < non_rewrite_len {
|
||||
writeln!(prompt, "And here's the section to rewrite based on that prompt again for reference:\n\n<rewrite_this>\n").unwrap();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
writeln!(prompt, "\n</rewrite_this>\n").unwrap();
|
||||
}
|
||||
writeln!(prompt, "Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {content_type} will be preserved.\n").unwrap();
|
||||
write!(
|
||||
prompt,
|
||||
"Start at the indentation level in the original file in the rewritten {content_type}. "
|
||||
)
|
||||
.unwrap();
|
||||
prompt.push_str("Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.");
|
||||
prompt.push_str("\n\nImmediately start with the following format with no remarks:\n\n```\n{{REWRITTEN_CODE}}\n```");
|
||||
}
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
@@ -196,23 +196,24 @@ mod linux {
|
||||
impl Detect {
|
||||
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
|
||||
let path = if let Some(path) = path {
|
||||
path.to_path_buf().canonicalize()
|
||||
path.to_path_buf().canonicalize()?
|
||||
} else {
|
||||
let cli = env::current_exe()?;
|
||||
let dir = cli
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.ok_or_else(|| anyhow!("no parent path for cli"))?;
|
||||
|
||||
match dir.join("libexec").join("zed-editor").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
// In development cli and zed are in the ./target/ directory together
|
||||
Err(e) => match cli.parent().unwrap().join("zed").canonicalize() {
|
||||
Ok(path) if path != cli => Ok(path),
|
||||
_ => Err(e),
|
||||
},
|
||||
}
|
||||
}?;
|
||||
// libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
|
||||
// ./zed is for the target directory in development builds.
|
||||
let possible_locations =
|
||||
["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
|
||||
possible_locations
|
||||
.iter()
|
||||
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
|
||||
.ok_or_else(|| {
|
||||
anyhow!("could not find any of: {}", possible_locations.join(", "))
|
||||
})?
|
||||
};
|
||||
|
||||
Ok(App(path))
|
||||
}
|
||||
|
||||
@@ -611,6 +611,7 @@ impl Telemetry {
|
||||
|
||||
let request_body = EventRequestBody {
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
metrics_id: state.metrics_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff,
|
||||
app_version: state.app_version.clone(),
|
||||
|
||||
@@ -87,51 +87,27 @@ impl Global {
|
||||
}
|
||||
|
||||
pub fn observed_any(&self, other: &Self) -> bool {
|
||||
let mut lhs = self.0.iter();
|
||||
let mut rhs = other.0.iter();
|
||||
loop {
|
||||
if let Some(left) = lhs.next() {
|
||||
if let Some(right) = rhs.next() {
|
||||
if *right > 0 && left >= right {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.0
|
||||
.iter()
|
||||
.zip(other.0.iter())
|
||||
.any(|(left, right)| *right > 0 && left >= right)
|
||||
}
|
||||
|
||||
pub fn observed_all(&self, other: &Self) -> bool {
|
||||
let mut lhs = self.0.iter();
|
||||
let mut rhs = other.0.iter();
|
||||
loop {
|
||||
if let Some(left) = lhs.next() {
|
||||
if let Some(right) = rhs.next() {
|
||||
if left < right {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return rhs.next().is_none();
|
||||
}
|
||||
}
|
||||
self.0.iter().all(|left| match rhs.next() {
|
||||
Some(right) => left >= right,
|
||||
None => true,
|
||||
}) && rhs.next().is_none()
|
||||
}
|
||||
|
||||
pub fn changed_since(&self, other: &Self) -> bool {
|
||||
if self.0.len() > other.0.len() {
|
||||
return true;
|
||||
}
|
||||
for (left, right) in self.0.iter().zip(other.0.iter()) {
|
||||
if left > right {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
self.0.len() > other.0.len()
|
||||
|| self
|
||||
.0
|
||||
.iter()
|
||||
.zip(other.0.iter())
|
||||
.any(|(left, right)| left > right)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {
|
||||
|
||||
@@ -664,6 +664,7 @@ where
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct EditorEventRow {
|
||||
installation_id: String,
|
||||
metrics_id: String,
|
||||
operation: String,
|
||||
app_version: String,
|
||||
file_extension: String,
|
||||
@@ -713,6 +714,7 @@ impl EditorEventRow {
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
architecture: body.architecture.clone(),
|
||||
installation_id: body.installation_id.clone().unwrap_or_default(),
|
||||
metrics_id: body.metrics_id.clone().unwrap_or_default(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
time: time.timestamp_millis(),
|
||||
|
||||
@@ -2583,14 +2583,13 @@ async fn rejoin_dev_server_projects(
|
||||
)
|
||||
.await?
|
||||
};
|
||||
notify_rejoined_projects(&mut rejoined_projects, &session)?;
|
||||
|
||||
response.send(proto::RejoinRemoteProjectsResponse {
|
||||
rejoined_projects: rejoined_projects
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|project| project.to_proto())
|
||||
.collect(),
|
||||
})
|
||||
})?;
|
||||
notify_rejoined_projects(&mut rejoined_projects, &session)
|
||||
}
|
||||
|
||||
async fn reconnect_dev_server(
|
||||
|
||||
@@ -73,6 +73,7 @@ impl ConnectionPool {
|
||||
pub fn reset(&mut self) {
|
||||
self.connections.clear();
|
||||
self.connected_users.clear();
|
||||
self.connected_dev_servers.clear();
|
||||
self.channels.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -504,6 +504,29 @@ async fn test_dev_server_reconnect(
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_restart(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
|
||||
let (server, client1) = TestServer::start1(cx1).await;
|
||||
|
||||
let (_dev_server, remote_workspace) =
|
||||
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
|
||||
let cx = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
|
||||
|
||||
server.reset().await;
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.simulate_keystrokes("cmd-p 1 enter");
|
||||
remote_workspace
|
||||
.update(cx, |ws, cx| {
|
||||
ws.active_item_as::<Editor>(cx)
|
||||
.unwrap()
|
||||
.update(cx, |ed, cx| {
|
||||
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_create_dev_server_project_path_validation(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
|
||||
@@ -124,5 +124,6 @@ fn notification_window_options(
|
||||
display_id: Some(screen.id()),
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +268,7 @@ gpui::actions!(
|
||||
SelectAllMatches,
|
||||
SelectDown,
|
||||
SelectLargerSyntaxNode,
|
||||
SelectEnclosingSymbol,
|
||||
SelectLeft,
|
||||
SelectLine,
|
||||
SelectRight,
|
||||
|
||||
@@ -720,8 +720,7 @@ impl DisplaySnapshot {
|
||||
if let Some(severity) = chunk.diagnostic_severity {
|
||||
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
|
||||
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
|
||||
let diagnostic_color =
|
||||
super::diagnostic_style(severity, true, &editor_style.status);
|
||||
let diagnostic_color = super::diagnostic_style(severity, &editor_style.status);
|
||||
diagnostic_highlight.underline = Some(UnderlineStyle {
|
||||
color: Some(diagnostic_color),
|
||||
thickness: 1.0.into(),
|
||||
@@ -957,16 +956,18 @@ impl DisplaySnapshot {
|
||||
return false;
|
||||
}
|
||||
|
||||
for next_row in (buffer_row.0 + 1)..=max_row.0 {
|
||||
let next_line_indent = self.line_indent_for_buffer_row(MultiBufferRow(next_row));
|
||||
if next_line_indent.raw_len() > line_indent.raw_len() {
|
||||
return true;
|
||||
} else if !next_line_indent.is_line_blank() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
(buffer_row.0 + 1..=max_row.0)
|
||||
.find_map(|next_row| {
|
||||
let next_line_indent = self.line_indent_for_buffer_row(MultiBufferRow(next_row));
|
||||
if next_line_indent.raw_len() > line_indent.raw_len() {
|
||||
Some(true)
|
||||
} else if !next_line_indent.is_line_blank() {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn foldable_range(
|
||||
|
||||
@@ -335,7 +335,7 @@ pub enum SelectMode {
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum EditorMode {
|
||||
SingleLine,
|
||||
SingleLine { auto_width: bool },
|
||||
AutoHeight { max_lines: usize },
|
||||
Full,
|
||||
}
|
||||
@@ -1580,7 +1580,13 @@ impl Editor {
|
||||
pub fn single_line(cx: &mut ViewContext<Self>) -> Self {
|
||||
let buffer = cx.new_model(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(EditorMode::SingleLine, buffer, None, false, cx)
|
||||
Self::new(
|
||||
EditorMode::SingleLine { auto_width: false },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn multi_line(cx: &mut ViewContext<Self>) -> Self {
|
||||
@@ -1589,6 +1595,18 @@ impl Editor {
|
||||
Self::new(EditorMode::Full, buffer, None, false, cx)
|
||||
}
|
||||
|
||||
pub fn auto_width(cx: &mut ViewContext<Self>) -> Self {
|
||||
let buffer = cx.new_model(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(
|
||||
EditorMode::SingleLine { auto_width: true },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn auto_height(max_lines: usize, cx: &mut ViewContext<Self>) -> Self {
|
||||
let buffer = cx.new_model(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
@@ -1701,8 +1719,8 @@ impl Editor {
|
||||
|
||||
let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
|
||||
|
||||
let soft_wrap_mode_override =
|
||||
(mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::PreferLine);
|
||||
let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. })
|
||||
.then(|| language_settings::SoftWrap::PreferLine);
|
||||
|
||||
let mut project_subscriptions = Vec::new();
|
||||
if mode == EditorMode::Full {
|
||||
@@ -1749,7 +1767,7 @@ impl Editor {
|
||||
.detach();
|
||||
cx.on_blur(&focus_handle, Self::handle_blur).detach();
|
||||
|
||||
let show_indent_guides = if mode == EditorMode::SingleLine {
|
||||
let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
@@ -1905,7 +1923,7 @@ impl Editor {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("Editor");
|
||||
let mode = match self.mode {
|
||||
EditorMode::SingleLine => "single_line",
|
||||
EditorMode::SingleLine { .. } => "single_line",
|
||||
EditorMode::AutoHeight { .. } => "auto_height",
|
||||
EditorMode::Full => "full",
|
||||
};
|
||||
@@ -2896,6 +2914,9 @@ impl Editor {
|
||||
let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
|
||||
let end_offset = start_offset + end_difference;
|
||||
let start_offset = start_offset + start_difference;
|
||||
if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() {
|
||||
continue;
|
||||
}
|
||||
let start = buffer_snapshot.anchor_after(start_offset);
|
||||
let end = buffer_snapshot.anchor_after(end_offset);
|
||||
linked_edits
|
||||
@@ -6660,7 +6681,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -6697,7 +6718,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -6728,7 +6749,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -6791,7 +6812,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -6839,7 +6860,7 @@ impl Editor {
|
||||
pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
|
||||
self.take_rename(true, cx);
|
||||
|
||||
if self.mode == EditorMode::SingleLine {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -6900,7 +6921,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -7248,7 +7269,7 @@ impl Editor {
|
||||
_: &MoveToStartOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -7268,7 +7289,7 @@ impl Editor {
|
||||
_: &MoveToEndOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -7288,7 +7309,7 @@ impl Editor {
|
||||
_: &SelectToStartOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -7308,7 +7329,7 @@ impl Editor {
|
||||
_: &SelectToEndOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -7324,7 +7345,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -7344,7 +7365,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
@@ -8203,7 +8224,7 @@ impl Editor {
|
||||
let advance_downwards = action.advance_downwards
|
||||
&& selections_on_single_row
|
||||
&& !selections_selecting
|
||||
&& this.mode != EditorMode::SingleLine;
|
||||
&& !matches!(this.mode, EditorMode::SingleLine { .. });
|
||||
|
||||
if advance_downwards {
|
||||
let snapshot = this.buffer.read(cx).snapshot(cx);
|
||||
@@ -8226,6 +8247,58 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_enclosing_symbol(
|
||||
&mut self,
|
||||
_: &SelectEnclosingSymbol,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let old_selections = self.selections.all::<usize>(cx).into_boxed_slice();
|
||||
|
||||
fn update_selection(
|
||||
selection: &Selection<usize>,
|
||||
buffer_snap: &MultiBufferSnapshot,
|
||||
) -> Option<Selection<usize>> {
|
||||
let cursor = selection.head();
|
||||
let (_buffer_id, symbols) = buffer_snap.symbols_containing(cursor, None)?;
|
||||
for symbol in symbols.iter().rev() {
|
||||
let start = symbol.range.start.to_offset(&buffer_snap);
|
||||
let end = symbol.range.end.to_offset(&buffer_snap);
|
||||
let new_range = start..end;
|
||||
if start < selection.start || end > selection.end {
|
||||
return Some(Selection {
|
||||
id: selection.id,
|
||||
start: new_range.start,
|
||||
end: new_range.end,
|
||||
goal: SelectionGoal::None,
|
||||
reversed: selection.reversed,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
let mut selected_larger_symbol = false;
|
||||
let new_selections = old_selections
|
||||
.iter()
|
||||
.map(|selection| match update_selection(selection, &buffer) {
|
||||
Some(new_selection) => {
|
||||
if new_selection.range() != selection.range() {
|
||||
selected_larger_symbol = true;
|
||||
}
|
||||
new_selection
|
||||
}
|
||||
None => selection.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if selected_larger_symbol {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(new_selections);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_larger_syntax_node(
|
||||
&mut self,
|
||||
_: &SelectLargerSyntaxNode,
|
||||
@@ -8743,13 +8816,7 @@ impl Editor {
|
||||
let display_point = initial_point.to_display_point(snapshot);
|
||||
let mut hunks = hunks
|
||||
.map(|hunk| diff_hunk_to_display(&hunk, &snapshot))
|
||||
.filter(|hunk| {
|
||||
if is_wrapped {
|
||||
true
|
||||
} else {
|
||||
!hunk.contains_display_row(display_point.row())
|
||||
}
|
||||
})
|
||||
.filter(|hunk| is_wrapped || !hunk.contains_display_row(display_point.row()))
|
||||
.dedup();
|
||||
|
||||
if let Some(hunk) = hunks.next() {
|
||||
@@ -12027,7 +12094,7 @@ impl Render for Editor {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let text_style = match self.mode {
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
@@ -12056,7 +12123,7 @@ impl Render for Editor {
|
||||
};
|
||||
|
||||
let background = match self.mode {
|
||||
EditorMode::SingleLine => cx.theme().system().transparent,
|
||||
EditorMode::SingleLine { .. } => cx.theme().system().transparent,
|
||||
EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent,
|
||||
EditorMode::Full => cx.theme().colors().editor_background,
|
||||
};
|
||||
@@ -12320,6 +12387,7 @@ impl ViewInputHandler for Editor {
|
||||
let font_id = cx.text_system().resolve_font(&style.text.font());
|
||||
let font_size = style.text.font_size.to_pixels(cx.rem_size());
|
||||
let line_height = style.text.line_height_in_pixels(cx.rem_size());
|
||||
|
||||
let em_width = cx
|
||||
.text_system()
|
||||
.typographic_bounds(font_id, font_size, 'm')
|
||||
@@ -12447,7 +12515,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
let group_id: SharedString = cx.block_id.to_string().into();
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
text_style.color = diagnostic_style(diagnostic.severity, true, cx.theme().status());
|
||||
text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status());
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
text_style.font_family = theme_settings.buffer_font.family.clone();
|
||||
text_style.font_style = theme_settings.buffer_font.style;
|
||||
@@ -12543,25 +12611,19 @@ pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, V
|
||||
prev_offset = ix + 1;
|
||||
if in_code_block {
|
||||
code_ranges.push(prev_len..text_without_backticks.len());
|
||||
in_code_block = false;
|
||||
} else {
|
||||
in_code_block = true;
|
||||
}
|
||||
in_code_block = !in_code_block;
|
||||
}
|
||||
|
||||
(text_without_backticks.into(), code_ranges)
|
||||
}
|
||||
|
||||
fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla {
|
||||
match (severity, valid) {
|
||||
(DiagnosticSeverity::ERROR, true) => colors.error,
|
||||
(DiagnosticSeverity::ERROR, false) => colors.error,
|
||||
(DiagnosticSeverity::WARNING, true) => colors.warning,
|
||||
(DiagnosticSeverity::WARNING, false) => colors.warning,
|
||||
(DiagnosticSeverity::INFORMATION, true) => colors.info,
|
||||
(DiagnosticSeverity::INFORMATION, false) => colors.info,
|
||||
(DiagnosticSeverity::HINT, true) => colors.info,
|
||||
(DiagnosticSeverity::HINT, false) => colors.info,
|
||||
fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla {
|
||||
match severity {
|
||||
DiagnosticSeverity::ERROR => colors.error,
|
||||
DiagnosticSeverity::WARNING => colors.warning,
|
||||
DiagnosticSeverity::INFORMATION => colors.info,
|
||||
DiagnosticSeverity::HINT => colors.info,
|
||||
_ => colors.ignored,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +276,7 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::toggle_comments);
|
||||
register_action(view, cx, Editor::select_larger_syntax_node);
|
||||
register_action(view, cx, Editor::select_smaller_syntax_node);
|
||||
register_action(view, cx, Editor::select_enclosing_symbol);
|
||||
register_action(view, cx, Editor::move_to_enclosing_bracket);
|
||||
register_action(view, cx, Editor::undo_selection);
|
||||
register_action(view, cx, Editor::redo_selection);
|
||||
@@ -1830,10 +1831,10 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn layout_lines(
|
||||
&self,
|
||||
rows: Range<DisplayRow>,
|
||||
line_number_layouts: &[Option<ShapedLine>],
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<LineWithInvisibles> {
|
||||
if rows.start >= rows.end {
|
||||
@@ -1842,7 +1843,7 @@ impl EditorElement {
|
||||
|
||||
// Show the placeholder when the editor is empty
|
||||
if snapshot.is_empty() {
|
||||
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
|
||||
let font_size = style.text.font_size.to_pixels(cx.rem_size());
|
||||
let placeholder_color = cx.theme().colors().text_placeholder;
|
||||
let placeholder_text = snapshot.placeholder_text();
|
||||
|
||||
@@ -1857,7 +1858,7 @@ impl EditorElement {
|
||||
.filter_map(move |line| {
|
||||
let run = TextRun {
|
||||
len: line.len(),
|
||||
font: self.style.text.font(),
|
||||
font: style.text.font(),
|
||||
color: placeholder_color,
|
||||
background_color: None,
|
||||
underline: Default::default(),
|
||||
@@ -1876,10 +1877,10 @@ impl EditorElement {
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let chunks = snapshot.highlighted_chunks(rows.clone(), true, &self.style);
|
||||
let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
|
||||
LineWithInvisibles::from_chunks(
|
||||
chunks,
|
||||
&self.style.text,
|
||||
&style.text,
|
||||
MAX_LINE_LEN,
|
||||
rows.len(),
|
||||
line_number_layouts,
|
||||
@@ -4474,7 +4475,7 @@ impl EditorElement {
|
||||
// We currently use single-line and auto-height editors in UI contexts,
|
||||
// so we don't want to scale everything with the buffer font size, as it
|
||||
// ends up looking off.
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => None,
|
||||
EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4498,12 +4499,43 @@ impl Element for EditorElement {
|
||||
editor.set_style(self.style.clone(), cx);
|
||||
|
||||
let layout_id = match editor.mode {
|
||||
EditorMode::SingleLine => {
|
||||
EditorMode::SingleLine { auto_width } => {
|
||||
let rem_size = cx.rem_size();
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
|
||||
cx.request_layout(style, None)
|
||||
|
||||
let height = self.style.text.line_height_in_pixels(rem_size);
|
||||
if auto_width {
|
||||
let editor_handle = cx.view().clone();
|
||||
let style = self.style.clone();
|
||||
cx.request_measured_layout(Style::default(), move |_, _, cx| {
|
||||
let editor_snapshot =
|
||||
editor_handle.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
let line = Self::layout_lines(
|
||||
DisplayRow(0)..DisplayRow(1),
|
||||
&[],
|
||||
&editor_snapshot,
|
||||
&style,
|
||||
cx,
|
||||
)
|
||||
.pop()
|
||||
.unwrap();
|
||||
|
||||
let font_id = cx.text_system().resolve_font(&style.text.font());
|
||||
let font_size = style.text.font_size.to_pixels(cx.rem_size());
|
||||
let em_width = cx
|
||||
.text_system()
|
||||
.typographic_bounds(font_id, font_size, 'm')
|
||||
.unwrap()
|
||||
.size
|
||||
.width;
|
||||
|
||||
size(line.width + em_width, height)
|
||||
})
|
||||
} else {
|
||||
let mut style = Style::default();
|
||||
style.size.height = height.into();
|
||||
style.size.width = relative(1.).into();
|
||||
cx.request_layout(style, None)
|
||||
}
|
||||
}
|
||||
EditorMode::AutoHeight { max_lines } => {
|
||||
let editor_handle = cx.view().clone();
|
||||
@@ -4762,8 +4794,13 @@ impl Element for EditorElement {
|
||||
);
|
||||
|
||||
let mut max_visible_line_width = Pixels::ZERO;
|
||||
let mut line_layouts =
|
||||
self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
|
||||
let mut line_layouts = Self::layout_lines(
|
||||
start_row..end_row,
|
||||
&line_numbers,
|
||||
&snapshot,
|
||||
&self.style,
|
||||
cx,
|
||||
);
|
||||
for line_with_invisibles in &line_layouts {
|
||||
if line_with_invisibles.width > max_visible_line_width {
|
||||
max_visible_line_width = line_with_invisibles.width;
|
||||
@@ -4791,16 +4828,43 @@ impl Element for EditorElement {
|
||||
)
|
||||
});
|
||||
|
||||
let scroll_pixel_position = point(
|
||||
scroll_position.x * em_width,
|
||||
scroll_position.y * line_height,
|
||||
);
|
||||
|
||||
let start_buffer_row =
|
||||
MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
|
||||
let end_buffer_row =
|
||||
MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
|
||||
|
||||
let scroll_max = point(
|
||||
((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
|
||||
max_row.as_f32(),
|
||||
);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
|
||||
|
||||
let autoscrolled = if autoscroll_horizontally {
|
||||
editor.autoscroll_horizontally(
|
||||
start_row,
|
||||
text_hitbox.size.width,
|
||||
scroll_width,
|
||||
em_width,
|
||||
&line_layouts,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if clamped || autoscrolled {
|
||||
snapshot = editor.snapshot(cx);
|
||||
scroll_position = snapshot.scroll_position();
|
||||
}
|
||||
});
|
||||
|
||||
let scroll_pixel_position = point(
|
||||
scroll_position.x * em_width,
|
||||
scroll_position.y * line_height,
|
||||
);
|
||||
|
||||
let indent_guides = self.layout_indent_guides(
|
||||
content_origin,
|
||||
text_hitbox.origin,
|
||||
@@ -6064,7 +6128,7 @@ mod tests {
|
||||
});
|
||||
|
||||
for editor_mode_without_invisibles in [
|
||||
EditorMode::SingleLine,
|
||||
EditorMode::SingleLine { auto_width: false },
|
||||
EditorMode::AutoHeight { max_lines: 100 },
|
||||
] {
|
||||
let invisibles = collect_invisibles_from_new_editor(
|
||||
|
||||
@@ -165,10 +165,16 @@ pub fn indent_guides_in_range(
|
||||
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
|
||||
.into_iter()
|
||||
.filter(|indent_guide| {
|
||||
let start =
|
||||
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
|
||||
// Filter out indent guides that are inside a fold
|
||||
!snapshot.is_line_folded(MultiBufferRow(
|
||||
indent_guide.multibuffer_row_range.start.0.saturating_sub(1),
|
||||
))
|
||||
let is_folded = snapshot.is_line_folded(start);
|
||||
let line_indent = snapshot.line_indent_for_buffer_row(start);
|
||||
|
||||
let contained_in_fold =
|
||||
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
|
||||
|
||||
!(is_folded && contained_in_fold)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ futures.workspace = true
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4" }
|
||||
gpui_macros.workspace = true
|
||||
http.workspace = true
|
||||
image = "0.23"
|
||||
image = "0.25.1"
|
||||
itertools.workspace = true
|
||||
lazy_static.workspace = true
|
||||
linkme = "0.3"
|
||||
@@ -80,6 +80,10 @@ backtrace = "0.3"
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "2.4"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
bindgen = "0.65.1"
|
||||
@@ -143,9 +147,6 @@ windows.workspace = true
|
||||
windows-core = "0.57"
|
||||
clipboard-win = "3.1.1"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
embed-resource = "2.4"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/hello_world.rs"
|
||||
@@ -157,3 +158,7 @@ path = "examples/image/image.rs"
|
||||
[[example]]
|
||||
name = "set_menus"
|
||||
path = "examples/set_menus.rs"
|
||||
|
||||
[[example]]
|
||||
name = "input"
|
||||
path = "examples/input.rs"
|
||||
|
||||
@@ -3,18 +3,25 @@
|
||||
//TODO: consider generating shader code for WGSL
|
||||
//TODO: deprecate "runtime-shaders" and "macos-blade"
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
use std::env;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
println!("cargo:rerun-if-changed={}", rc_file.display());
|
||||
embed_resource::compile(rc_file, embed_resource::NONE);
|
||||
}
|
||||
fn main() {
|
||||
let target = env::var("CARGO_CFG_TARGET_OS");
|
||||
|
||||
match target.as_deref() {
|
||||
Ok("macos") => {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
}
|
||||
Ok("windows") => {
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
println!("cargo:rerun-if-changed={}", rc_file.display());
|
||||
embed_resource::compile(rc_file, embed_resource::NONE);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
489
crates/gpui/examples/input.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::*;
|
||||
use unicode_segmentation::*;
|
||||
|
||||
actions!(
|
||||
text_input,
|
||||
[
|
||||
Backspace,
|
||||
Delete,
|
||||
Left,
|
||||
Right,
|
||||
SelectLeft,
|
||||
SelectRight,
|
||||
SelectAll,
|
||||
Home,
|
||||
End,
|
||||
ShowCharacterPalette
|
||||
]
|
||||
);
|
||||
|
||||
struct TextInput {
|
||||
focus_handle: FocusHandle,
|
||||
content: SharedString,
|
||||
selected_range: Range<usize>,
|
||||
selection_reversed: bool,
|
||||
marked_range: Option<Range<usize>>,
|
||||
last_layout: Option<ShapedLine>,
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||
} else {
|
||||
self.move_to(self.selected_range.start, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.next_boundary(self.selected_range.end), cx);
|
||||
} else {
|
||||
self.move_to(self.selected_range.end, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
|
||||
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||
}
|
||||
|
||||
fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
|
||||
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
||||
}
|
||||
|
||||
fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
|
||||
self.move_to(0, cx);
|
||||
self.select_to(self.content.len(), cx)
|
||||
}
|
||||
|
||||
fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
|
||||
self.move_to(0, cx);
|
||||
}
|
||||
|
||||
fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
|
||||
self.move_to(self.content.len(), cx);
|
||||
}
|
||||
|
||||
fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", cx)
|
||||
}
|
||||
|
||||
fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.next_boundary(self.cursor_offset()), cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", cx)
|
||||
}
|
||||
|
||||
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
|
||||
cx.show_character_palette();
|
||||
}
|
||||
|
||||
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
||||
self.selected_range = offset..offset;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn cursor_offset(&self) -> usize {
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start
|
||||
} else {
|
||||
self.selected_range.end
|
||||
}
|
||||
}
|
||||
|
||||
fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start = offset
|
||||
} else {
|
||||
self.selected_range.end = offset
|
||||
};
|
||||
if self.selected_range.end < self.selected_range.start {
|
||||
self.selection_reversed = !self.selection_reversed;
|
||||
self.selected_range = self.selected_range.end..self.selected_range.start;
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn offset_from_utf16(&self, offset: usize) -> usize {
|
||||
let mut utf8_offset = 0;
|
||||
let mut utf16_count = 0;
|
||||
|
||||
for ch in self.content.chars() {
|
||||
if utf16_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf16_count += ch.len_utf16();
|
||||
utf8_offset += ch.len_utf8();
|
||||
}
|
||||
|
||||
utf8_offset
|
||||
}
|
||||
|
||||
fn offset_to_utf16(&self, offset: usize) -> usize {
|
||||
let mut utf16_offset = 0;
|
||||
let mut utf8_count = 0;
|
||||
|
||||
for ch in self.content.chars() {
|
||||
if utf8_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf8_count += ch.len_utf8();
|
||||
utf16_offset += ch.len_utf16();
|
||||
}
|
||||
|
||||
utf16_offset
|
||||
}
|
||||
|
||||
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
||||
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
|
||||
}
|
||||
|
||||
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
|
||||
}
|
||||
|
||||
fn previous_boundary(&self, offset: usize) -> usize {
|
||||
self.content
|
||||
.grapheme_indices(true)
|
||||
.rev()
|
||||
.find_map(|(idx, _)| (idx < offset).then_some(idx))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn next_boundary(&self, offset: usize) -> usize {
|
||||
self.content
|
||||
.grapheme_indices(true)
|
||||
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||
.unwrap_or(self.content.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewInputHandler for TextInput {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
let range = self.range_from_utf16(&range_utf16);
|
||||
Some(self.content[range].to_string())
|
||||
}
|
||||
|
||||
fn selected_text_range(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
||||
Some(self.range_to_utf16(&self.selected_range))
|
||||
}
|
||||
|
||||
fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
||||
self.marked_range
|
||||
.as_ref()
|
||||
.map(|range| self.range_to_utf16(range))
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
|
||||
self.marked_range = None;
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.or(self.marked_range.clone())
|
||||
.unwrap_or(self.selected_range.clone());
|
||||
|
||||
self.content =
|
||||
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||
.into();
|
||||
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
|
||||
self.marked_range.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range_utf16: Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.or(self.marked_range.clone())
|
||||
.unwrap_or(self.selected_range.clone());
|
||||
|
||||
self.content =
|
||||
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||
.into();
|
||||
self.marked_range = Some(range.start..range.start + new_text.len());
|
||||
self.selected_range = new_selected_range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
|
||||
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
let Some(last_layout) = self.last_layout.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
let range = self.range_from_utf16(&range_utf16);
|
||||
Some(Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + last_layout.x_for_index(range.start),
|
||||
bounds.top(),
|
||||
),
|
||||
point(
|
||||
bounds.left() + last_layout.x_for_index(range.end),
|
||||
bounds.bottom(),
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
struct TextElement {
|
||||
input: View<TextInput>,
|
||||
}
|
||||
|
||||
struct PrepaintState {
|
||||
line: Option<ShapedLine>,
|
||||
cursor: Option<PaintQuad>,
|
||||
selection: Option<PaintQuad>,
|
||||
}
|
||||
|
||||
impl IntoElement for TextElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TextElement {
|
||||
type RequestLayoutState = ();
|
||||
|
||||
type PrepaintState = PrepaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = cx.line_height().into();
|
||||
(cx.request_layout(style, []), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self::PrepaintState {
|
||||
let input = self.input.read(cx);
|
||||
let content = input.content.clone();
|
||||
let selected_range = input.selected_range.clone();
|
||||
let cursor = input.cursor_offset();
|
||||
let style = cx.text_style();
|
||||
let run = TextRun {
|
||||
len: input.content.len(),
|
||||
font: style.font(),
|
||||
color: style.color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
|
||||
vec![
|
||||
TextRun {
|
||||
len: marked_range.start,
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: marked_range.end - marked_range.start,
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(run.color),
|
||||
thickness: px(1.0),
|
||||
wavy: false,
|
||||
}),
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: input.content.len() - marked_range.end,
|
||||
..run.clone()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|run| run.len > 0)
|
||||
.collect()
|
||||
} else {
|
||||
vec![run]
|
||||
};
|
||||
|
||||
let font_size = style.font_size.to_pixels(cx.rem_size());
|
||||
let line = cx
|
||||
.text_system()
|
||||
.shape_line(content, font_size, &runs)
|
||||
.unwrap();
|
||||
|
||||
let cursor_pos = line.x_for_index(cursor);
|
||||
let (selection, cursor) = if selected_range.is_empty() {
|
||||
(
|
||||
None,
|
||||
Some(fill(
|
||||
Bounds::new(
|
||||
point(bounds.left() + cursor_pos, bounds.top()),
|
||||
size(px(2.), bounds.bottom() - bounds.top()),
|
||||
),
|
||||
gpui::blue(),
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Some(fill(
|
||||
Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + line.x_for_index(selected_range.start),
|
||||
bounds.top(),
|
||||
),
|
||||
point(
|
||||
bounds.left() + line.x_for_index(selected_range.end),
|
||||
bounds.bottom(),
|
||||
),
|
||||
),
|
||||
rgba(0x3311FF30),
|
||||
)),
|
||||
None,
|
||||
)
|
||||
};
|
||||
PrepaintState {
|
||||
line: Some(line),
|
||||
cursor,
|
||||
selection,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let focus_handle = self.input.read(cx).focus_handle.clone();
|
||||
cx.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.input.clone()),
|
||||
);
|
||||
if let Some(selection) = prepaint.selection.take() {
|
||||
cx.paint_quad(selection)
|
||||
}
|
||||
let line = prepaint.line.take().unwrap();
|
||||
line.paint(bounds.origin, cx.line_height(), cx).unwrap();
|
||||
|
||||
if let Some(cursor) = prepaint.cursor.take() {
|
||||
cx.paint_quad(cursor);
|
||||
}
|
||||
self.input.update(cx, |input, _cx| {
|
||||
input.last_layout = Some(line);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TextInput {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.key_context("TextInput")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::backspace))
|
||||
.on_action(cx.listener(Self::delete))
|
||||
.on_action(cx.listener(Self::left))
|
||||
.on_action(cx.listener(Self::right))
|
||||
.on_action(cx.listener(Self::select_left))
|
||||
.on_action(cx.listener(Self::select_right))
|
||||
.on_action(cx.listener(Self::select_all))
|
||||
.on_action(cx.listener(Self::home))
|
||||
.on_action(cx.listener(Self::end))
|
||||
.on_action(cx.listener(Self::show_character_palette))
|
||||
.bg(rgb(0xeeeeee))
|
||||
.size_full()
|
||||
.line_height(px(30.))
|
||||
.text_size(px(24.))
|
||||
.child(
|
||||
div()
|
||||
.h(px(30. + 4. * 2.))
|
||||
.w_full()
|
||||
.p(px(4.))
|
||||
.bg(white())
|
||||
.child(TextElement {
|
||||
input: cx.view().clone(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("backspace", Backspace, None),
|
||||
KeyBinding::new("delete", Delete, None),
|
||||
KeyBinding::new("left", Left, None),
|
||||
KeyBinding::new("right", Right, None),
|
||||
KeyBinding::new("shift-left", SelectLeft, None),
|
||||
KeyBinding::new("shift-right", SelectRight, None),
|
||||
KeyBinding::new("cmd-a", SelectAll, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
||||
]);
|
||||
let window = cx
|
||||
.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|cx| TextInput {
|
||||
focus_handle: cx.focus_handle(),
|
||||
content: "".into(),
|
||||
selected_range: 0..0,
|
||||
selection_reversed: false,
|
||||
marked_range: None,
|
||||
last_layout: None,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
window
|
||||
.update(cx, |view, cx| {
|
||||
view.focus_handle.focus(cx);
|
||||
cx.activate(true)
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
@@ -51,6 +51,7 @@ fn main() {
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{size, DevicePixels, Result, SharedString, Size};
|
||||
|
||||
use image::{Bgra, ImageBuffer};
|
||||
use image::RgbaImage;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
@@ -40,12 +40,12 @@ pub(crate) struct RenderImageParams {
|
||||
pub struct ImageData {
|
||||
/// The ID associated with this image
|
||||
pub id: ImageId,
|
||||
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
|
||||
data: RgbaImage,
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
/// Create a new image from the given data.
|
||||
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
|
||||
pub fn new(data: RgbaImage) -> Self {
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
Self {
|
||||
|
||||
@@ -384,7 +384,13 @@ impl Asset for Image {
|
||||
};
|
||||
|
||||
let data = if let Ok(format) = image::guess_format(&bytes) {
|
||||
let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
|
||||
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
|
||||
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
|
||||
ImageData::new(data)
|
||||
} else {
|
||||
let pixmap =
|
||||
|
||||
@@ -2287,6 +2287,15 @@ impl Pixels {
|
||||
pub fn abs(&self) -> Self {
|
||||
Self(self.0.abs())
|
||||
}
|
||||
|
||||
/// Returns the f64 value of `Pixels`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A f64 value of the `Pixels`.
|
||||
pub fn to_f64(self) -> f64 {
|
||||
self.0 as f64
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Pixels> for Pixels {
|
||||
|
||||
@@ -567,6 +567,9 @@ pub struct WindowOptions {
|
||||
|
||||
/// Application identifier of the window. Can by used by desktop environments to group applications together.
|
||||
pub app_id: Option<String>,
|
||||
|
||||
/// Window minimum size
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
/// The variables that can be configured when creating a new window
|
||||
@@ -594,6 +597,9 @@ pub(crate) struct WindowParams {
|
||||
pub display_id: Option<DisplayId>,
|
||||
|
||||
pub window_background: WindowBackgroundAppearance,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
/// Represents the status of how a window should be opened.
|
||||
@@ -642,6 +648,7 @@ impl Default for WindowOptions {
|
||||
display_id: None,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,27 @@ impl Keystroke {
|
||||
}
|
||||
}
|
||||
|
||||
//Allow for the user to specify a keystroke modifier as the key itself
|
||||
//This sets the `key` to the modifier, and disables the modifier
|
||||
if key.is_none() {
|
||||
if shift {
|
||||
key = Some("shift".to_string());
|
||||
shift = false;
|
||||
} else if control {
|
||||
key = Some("control".to_string());
|
||||
control = false;
|
||||
} else if alt {
|
||||
key = Some("alt".to_string());
|
||||
alt = false;
|
||||
} else if platform {
|
||||
key = Some("platform".to_string());
|
||||
platform = false;
|
||||
} else if function {
|
||||
key = Some("function".to_string());
|
||||
function = false;
|
||||
}
|
||||
}
|
||||
|
||||
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
|
||||
|
||||
Ok(Keystroke {
|
||||
@@ -186,6 +207,10 @@ impl std::fmt::Display for Keystroke {
|
||||
"right" => '→',
|
||||
"tab" => '⇥',
|
||||
"escape" => '⎋',
|
||||
"shift" => '⇧',
|
||||
"control" => '⌃',
|
||||
"alt" => '⌥',
|
||||
"platform" => '⌘',
|
||||
key => {
|
||||
if key.len() == 1 {
|
||||
key.chars().next().unwrap().to_ascii_uppercase()
|
||||
@@ -241,6 +266,15 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many modifier keys are pressed
|
||||
pub fn number_of_modifiers(&self) -> u8 {
|
||||
self.control as u8
|
||||
+ self.alt as u8
|
||||
+ self.shift as u8
|
||||
+ self.platform as u8
|
||||
+ self.function as u8
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with no modifiers
|
||||
pub fn none() -> Modifiers {
|
||||
Default::default()
|
||||
|
||||
@@ -22,7 +22,7 @@ impl HeadlessClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()));
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::any::{type_name, Any};
|
||||
use std::cell::{self, RefCell};
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
@@ -83,6 +84,16 @@ pub(crate) struct PlatformHandlers {
|
||||
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
}
|
||||
|
||||
pub trait QuitSignal {
|
||||
fn quit(&mut self);
|
||||
}
|
||||
|
||||
impl QuitSignal for LoopSignal {
|
||||
fn quit(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LinuxCommon {
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
@@ -90,12 +101,12 @@ pub(crate) struct LinuxCommon {
|
||||
pub(crate) appearance: WindowAppearance,
|
||||
pub(crate) auto_hide_scrollbars: bool,
|
||||
pub(crate) callbacks: PlatformHandlers,
|
||||
pub(crate) signal: LoopSignal,
|
||||
pub(crate) signal: Box<dyn QuitSignal>,
|
||||
pub(crate) menus: Vec<OwnedMenu>,
|
||||
}
|
||||
|
||||
impl LinuxCommon {
|
||||
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
|
||||
pub fn new(signal: Box<dyn QuitSignal>) -> (Self, Channel<Runnable>) {
|
||||
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
|
||||
let text_system = Arc::new(CosmicTextSystem::new());
|
||||
let callbacks = PlatformHandlers::default();
|
||||
@@ -145,7 +156,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
self.with_common(|common| common.signal.stop());
|
||||
self.with_common(|common| common.signal.quit());
|
||||
}
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
@@ -508,6 +519,27 @@ pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bo
|
||||
diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
|
||||
}
|
||||
|
||||
pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
|
||||
let mut locales = Vec::default();
|
||||
if let Some(locale) = std::env::var_os("LC_CTYPE") {
|
||||
locales.push(locale);
|
||||
}
|
||||
locales.push(OsString::from("C"));
|
||||
let mut state: Option<xkb::compose::State> = None;
|
||||
for locale in locales {
|
||||
if let Ok(table) =
|
||||
xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
|
||||
{
|
||||
state = Some(xkb::compose::State::new(
|
||||
&table,
|
||||
xkb::compose::STATE_NO_FLAGS,
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
|
||||
let mut file = File::from_raw_fd(fd.as_raw_fd());
|
||||
|
||||
@@ -583,19 +615,11 @@ impl Keystroke {
|
||||
let key_utf8 = state.key_get_utf8(keycode);
|
||||
let key_sym = state.key_get_one_sym(keycode);
|
||||
|
||||
// The logic here tries to replicate the logic in `../mac/events.rs`
|
||||
// "Consumed" modifiers are modifiers that have been used to translate a key, for example
|
||||
// pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift.
|
||||
// Notes:
|
||||
// - macOS gets the key character directly ("."), xkb gives us the key name ("period")
|
||||
// - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{"
|
||||
// - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A"
|
||||
|
||||
let mut handle_consumed_modifiers = true;
|
||||
let key = match key_sym {
|
||||
Keysym::Return => "enter".to_owned(),
|
||||
Keysym::Prior => "pageup".to_owned(),
|
||||
Keysym::Next => "pagedown".to_owned(),
|
||||
Keysym::ISO_Left_Tab => "tab".to_owned(),
|
||||
|
||||
Keysym::comma => ",".to_owned(),
|
||||
Keysym::period => ".".to_owned(),
|
||||
@@ -633,30 +657,22 @@ impl Keystroke {
|
||||
Keysym::equal => "=".to_owned(),
|
||||
Keysym::plus => "+".to_owned(),
|
||||
|
||||
Keysym::ISO_Left_Tab => {
|
||||
handle_consumed_modifiers = false;
|
||||
"tab".to_owned()
|
||||
}
|
||||
|
||||
_ => {
|
||||
handle_consumed_modifiers = false;
|
||||
xkb::keysym_get_name(key_sym).to_lowercase()
|
||||
}
|
||||
_ => xkb::keysym_get_name(key_sym).to_lowercase(),
|
||||
};
|
||||
|
||||
if modifiers.shift {
|
||||
// we only include the shift for upper-case letters by convention,
|
||||
// so don't include for numbers and symbols, but do include for
|
||||
// tab/enter, etc.
|
||||
if key.chars().count() == 1 && key_utf8 == key {
|
||||
modifiers.shift = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore control characters (and DEL) for the purposes of ime_key
|
||||
let ime_key =
|
||||
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
|
||||
|
||||
if handle_consumed_modifiers {
|
||||
let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT);
|
||||
let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index);
|
||||
|
||||
if modifiers.shift && is_shift_consumed {
|
||||
modifiers.shift = false;
|
||||
}
|
||||
}
|
||||
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::ffi::OsString;
|
||||
use std::hash::Hash;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::path::PathBuf;
|
||||
@@ -65,7 +64,6 @@ use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
|
||||
use super::display::WaylandDisplay;
|
||||
use super::window::{ImeInput, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::wayland::clipboard::{
|
||||
Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPE,
|
||||
};
|
||||
@@ -74,6 +72,7 @@ use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
use crate::platform::linux::wayland::window::WaylandWindow;
|
||||
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::linux::{get_xkb_compose_state, is_within_click_distance};
|
||||
use crate::platform::PlatformWindow;
|
||||
use crate::{
|
||||
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
|
||||
@@ -311,7 +310,7 @@ impl WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
state.common.signal.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,7 +406,7 @@ impl WaylandClient {
|
||||
|
||||
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()));
|
||||
|
||||
let handle = event_loop.handle();
|
||||
handle
|
||||
@@ -671,12 +670,12 @@ impl LinuxClient for WaylandClient {
|
||||
return;
|
||||
};
|
||||
if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() {
|
||||
let serial = state.serial_tracker.get(SerialKind::KeyEnter);
|
||||
state.clipboard.set_primary(item);
|
||||
let serial = state.serial_tracker.get(SerialKind::KeyPress);
|
||||
let data_source = primary_selection_manager.create_source(&state.globals.qh, ());
|
||||
data_source.offer(state.clipboard.self_mime());
|
||||
data_source.offer(TEXT_MIME_TYPE.to_string());
|
||||
primary_selection.set_selection(Some(&data_source), serial);
|
||||
state.clipboard.set_primary(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,35 +688,21 @@ impl LinuxClient for WaylandClient {
|
||||
return;
|
||||
};
|
||||
if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() {
|
||||
let serial = state.serial_tracker.get(SerialKind::KeyEnter);
|
||||
state.clipboard.set(item);
|
||||
let serial = state.serial_tracker.get(SerialKind::KeyPress);
|
||||
let data_source = data_device_manager.create_data_source(&state.globals.qh, ());
|
||||
data_source.offer(state.clipboard.self_mime());
|
||||
data_source.offer(TEXT_MIME_TYPE.to_string());
|
||||
data_device.set_selection(Some(&data_source), serial);
|
||||
state.clipboard.set(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.read_primary()
|
||||
.map(|s| crate::ClipboardItem {
|
||||
text: s,
|
||||
metadata: None,
|
||||
})
|
||||
self.0.borrow_mut().clipboard.read_primary()
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.read()
|
||||
.map(|s| crate::ClipboardItem {
|
||||
text: s,
|
||||
metadata: None,
|
||||
})
|
||||
self.0.borrow_mut().clipboard.read()
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
@@ -1068,21 +1053,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
.flatten()
|
||||
.expect("Failed to create keymap")
|
||||
};
|
||||
let table = {
|
||||
let locale = std::env::var_os("LC_CTYPE").unwrap_or(OsString::from("C"));
|
||||
xkb::compose::Table::new_from_locale(
|
||||
&xkb_context,
|
||||
&locale,
|
||||
xkb::compose::COMPILE_NO_FLAGS,
|
||||
)
|
||||
.log_err()
|
||||
.unwrap()
|
||||
};
|
||||
state.keymap_state = Some(xkb::State::new(&keymap));
|
||||
state.compose_state = Some(xkb::compose::State::new(
|
||||
&table,
|
||||
xkb::compose::STATE_NO_FLAGS,
|
||||
));
|
||||
state.compose_state = get_xkb_compose_state(&xkb_context);
|
||||
}
|
||||
wl_keyboard::Event::Enter {
|
||||
serial, surface, ..
|
||||
@@ -1162,6 +1134,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
compose.feed(keysym);
|
||||
match compose.status() {
|
||||
xkb::Status::Composing => {
|
||||
keystroke.ime_key = None;
|
||||
state.pre_edit_text =
|
||||
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
@@ -1174,7 +1147,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
xkb::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.ime_key = compose.utf8();
|
||||
keystroke.key = xkb::keysym_get_name(compose.keysym().unwrap());
|
||||
if let Some(keysym) = compose.keysym() {
|
||||
keystroke.key = xkb::keysym_get_name(keysym);
|
||||
}
|
||||
}
|
||||
xkb::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
|
||||
@@ -9,7 +9,7 @@ use filedescriptor::Pipe;
|
||||
use wayland_client::{protocol::wl_data_offer::WlDataOffer, Connection};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
|
||||
|
||||
use crate::{platform::linux::platform::read_fd, WaylandClientStatePtr};
|
||||
use crate::{platform::linux::platform::read_fd, ClipboardItem, WaylandClientStatePtr};
|
||||
|
||||
pub(crate) const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8";
|
||||
pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
|
||||
@@ -23,13 +23,13 @@ pub(crate) struct Clipboard {
|
||||
self_mime: String,
|
||||
|
||||
// Internal clipboard
|
||||
contents: Option<String>,
|
||||
primary_contents: Option<String>,
|
||||
contents: Option<ClipboardItem>,
|
||||
primary_contents: Option<ClipboardItem>,
|
||||
|
||||
// External clipboard
|
||||
cached_read: Option<String>,
|
||||
cached_read: Option<ClipboardItem>,
|
||||
current_offer: Option<DataOffer<WlDataOffer>>,
|
||||
cached_primary_read: Option<String>,
|
||||
cached_primary_read: Option<ClipboardItem>,
|
||||
current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
}
|
||||
|
||||
@@ -89,12 +89,12 @@ impl Clipboard {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, text: String) {
|
||||
self.contents = Some(text);
|
||||
pub fn set(&mut self, item: ClipboardItem) {
|
||||
self.contents = Some(item);
|
||||
}
|
||||
|
||||
pub fn set_primary(&mut self, text: String) {
|
||||
self.primary_contents = Some(text);
|
||||
pub fn set_primary(&mut self, item: ClipboardItem) {
|
||||
self.primary_contents = Some(item);
|
||||
}
|
||||
|
||||
pub fn set_offer(&mut self, data_offer: Option<DataOffer<WlDataOffer>>) {
|
||||
@@ -113,17 +113,17 @@ impl Clipboard {
|
||||
|
||||
pub fn send(&self, _mime_type: String, fd: OwnedFd) {
|
||||
if let Some(contents) = &self.contents {
|
||||
self.send_internal(fd, contents.as_bytes().to_owned());
|
||||
self.send_internal(fd, contents.text.as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) {
|
||||
if let Some(primary_contents) = &self.primary_contents {
|
||||
self.send_internal(fd, primary_contents.as_bytes().to_owned());
|
||||
self.send_internal(fd, primary_contents.text.as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Option<String> {
|
||||
pub fn read(&mut self) -> Option<ClipboardItem> {
|
||||
let offer = self.current_offer.clone()?;
|
||||
if let Some(cached) = self.cached_read.clone() {
|
||||
return Some(cached);
|
||||
@@ -145,8 +145,8 @@ impl Clipboard {
|
||||
|
||||
match unsafe { read_fd(fd) } {
|
||||
Ok(v) => {
|
||||
self.cached_read = Some(v.clone());
|
||||
Some(v)
|
||||
self.cached_read = Some(ClipboardItem::new(v));
|
||||
self.cached_read.clone()
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
@@ -155,7 +155,7 @@ impl Clipboard {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_primary(&mut self) -> Option<String> {
|
||||
pub fn read_primary(&mut self) -> Option<ClipboardItem> {
|
||||
let offer = self.current_primary_offer.clone()?;
|
||||
if let Some(cached) = self.cached_primary_read.clone() {
|
||||
return Some(cached);
|
||||
@@ -177,8 +177,8 @@ impl Clipboard {
|
||||
|
||||
match unsafe { read_fd(fd) } {
|
||||
Ok(v) => {
|
||||
self.cached_primary_read = Some(v.clone());
|
||||
Some(v)
|
||||
self.cached_primary_read = Some(ClipboardItem::new(v.clone()));
|
||||
self.cached_primary_read.clone()
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
use calloop::{EventLoop, LoopHandle, RegistrationToken};
|
||||
use anyhow::Context;
|
||||
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
|
||||
use collections::HashMap;
|
||||
use util::ResultExt;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use util::ResultExt;
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::cursor;
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::ConnectionExt;
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
|
||||
@@ -29,13 +30,13 @@ use xkbcommon::xkb as xkbc;
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId,
|
||||
Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point,
|
||||
ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
|
||||
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{open_uri_internal, SCROLL_LINES},
|
||||
super::{get_xkb_compose_state, open_uri_internal, SCROLL_LINES},
|
||||
X11Display, X11WindowStatePtr, XcbAtoms,
|
||||
};
|
||||
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
|
||||
@@ -48,7 +49,6 @@ pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
|
||||
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11WindowStatePtr,
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
impl WindowRef {
|
||||
@@ -96,9 +96,6 @@ impl From<xim::ClientError> for EventHandlerError {
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
|
||||
pub(crate) last_click: Instant,
|
||||
pub(crate) last_location: Point<Pixels>,
|
||||
pub(crate) current_count: usize,
|
||||
@@ -116,7 +113,7 @@ pub struct X11ClientState {
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
pub(crate) compose_state: xkbc::compose::State,
|
||||
pub(crate) compose_state: Option<xkbc::compose::State>,
|
||||
pub(crate) pre_edit_text: Option<String>,
|
||||
pub(crate) composing: bool,
|
||||
pub(crate) cursor_handle: cursor::Handle,
|
||||
@@ -129,6 +126,12 @@ pub struct X11ClientState {
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: x11_clipboard::Clipboard,
|
||||
pub(crate) clipboard_item: Option<ClipboardItem>,
|
||||
|
||||
quit_signal_rx: oneshot::Receiver<()>,
|
||||
|
||||
runnables: Channel<Runnable>,
|
||||
xdp_event_source: XDPEventSource,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -139,14 +142,39 @@ impl X11ClientStatePtr {
|
||||
let client = X11Client(self.0.upgrade().expect("client already dropped"));
|
||||
let mut state = client.0.borrow_mut();
|
||||
|
||||
if let Some(window_ref) = state.windows.remove(&x_window) {
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
if state.windows.remove(&x_window).is_none() {
|
||||
log::warn!(
|
||||
"failed to remove X window {} from client state, does not exist",
|
||||
x_window
|
||||
);
|
||||
}
|
||||
|
||||
state.cursor_styles.remove(&x_window);
|
||||
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
state.common.signal.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelQuitSignal {
|
||||
tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl ChannelQuitSignal {
|
||||
fn new() -> (Self, oneshot::Receiver<()>) {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
|
||||
let quit_signal = ChannelQuitSignal { tx: Some(tx) };
|
||||
|
||||
(quit_signal, rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl QuitSignal for ChannelQuitSignal {
|
||||
fn quit(&mut self) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(()).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,27 +184,9 @@ pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11Client {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle
|
||||
.insert_source(main_receiver, {
|
||||
let handle = handle.clone();
|
||||
move |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
|
||||
// events have higher priority and runnables are only worked off after the event
|
||||
// callbacks.
|
||||
handle.insert_idle(|_| {
|
||||
runnable.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let (common, runnables) = LinuxCommon::new(Box::new(quit_signal));
|
||||
|
||||
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
|
||||
xcb_connection
|
||||
@@ -249,18 +259,7 @@ impl X11Client {
|
||||
);
|
||||
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
|
||||
};
|
||||
let compose_state = {
|
||||
let locale = std::env::var_os("LC_CTYPE").unwrap_or(OsString::from("C"));
|
||||
let table = xkbc::compose::Table::new_from_locale(
|
||||
&xkb_context,
|
||||
&locale,
|
||||
xkbc::compose::COMPILE_NO_FLAGS,
|
||||
)
|
||||
.log_err()
|
||||
.unwrap();
|
||||
xkbc::compose::State::new(&table, xkbc::compose::STATE_NO_FLAGS)
|
||||
};
|
||||
|
||||
let compose_state = get_xkb_compose_state(&xkb_context);
|
||||
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
|
||||
|
||||
let scale_factor = resource_database
|
||||
@@ -286,105 +285,16 @@ impl X11Client {
|
||||
None
|
||||
};
|
||||
|
||||
// Safety: Safe if xcb::Connection always returns a valid fd
|
||||
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
|
||||
|
||||
handle
|
||||
.insert_source(
|
||||
Generic::new_with_error::<EventHandlerError>(
|
||||
fd,
|
||||
calloop::Interest::READ,
|
||||
calloop::Mode::Level,
|
||||
),
|
||||
{
|
||||
let xcb_connection = xcb_connection.clone();
|
||||
move |_readiness, _, client| {
|
||||
let mut events = Vec::new();
|
||||
let mut windows_to_refresh = HashSet::new();
|
||||
|
||||
while let Some(event) = xcb_connection.poll_for_event()? {
|
||||
if let Event::Expose(event) = event {
|
||||
windows_to_refresh.insert(event.window);
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
for window in windows_to_refresh.into_iter() {
|
||||
if let Some(window) = client.get_window(window) {
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
for event in events.into_iter() {
|
||||
let mut state = client.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
client.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let xim_callback_event = xim_handler.last_callback_event.take();
|
||||
|
||||
let mut state = client.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
|
||||
if let Some(event) = xim_callback_event {
|
||||
client.handle_xim_callback_event(event);
|
||||
}
|
||||
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
|
||||
if xim_connected {
|
||||
client.xim_handle_event(event);
|
||||
} else {
|
||||
client.handle_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(calloop::PostAction::Continue)
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("Failed to initialize x11 event source");
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
client.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut client.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
|
||||
// noop, X11 manages this for us.
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let xdp_event_source = XDPEventSource::new(&common.background_executor);
|
||||
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
modifiers: Modifiers::default(),
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
runnables,
|
||||
xdp_event_source,
|
||||
quit_signal_rx,
|
||||
|
||||
common,
|
||||
|
||||
modifiers: Modifiers::default(),
|
||||
last_click: Instant::now(),
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
@@ -400,7 +310,7 @@ impl X11Client {
|
||||
ximc,
|
||||
xim_handler,
|
||||
|
||||
compose_state: compose_state,
|
||||
compose_state,
|
||||
pre_edit_text: None,
|
||||
composing: false,
|
||||
|
||||
@@ -413,6 +323,7 @@ impl X11Client {
|
||||
scroll_y: None,
|
||||
|
||||
clipboard,
|
||||
clipboard_item: None,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -478,6 +389,50 @@ impl X11Client {
|
||||
.map(|window_reference| window_reference.window.clone())
|
||||
}
|
||||
|
||||
fn handle_events(&self, events: Vec<Event>) {
|
||||
for event in events.into_iter() {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let xim_callback_event = xim_handler.last_callback_event.take();
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
|
||||
if let Some(event) = xim_callback_event {
|
||||
self.handle_xim_callback_event(event);
|
||||
}
|
||||
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
|
||||
if xim_connected {
|
||||
self.xim_handle_event(event);
|
||||
} else {
|
||||
self.handle_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&self, event: Event) -> Option<()> {
|
||||
match event {
|
||||
Event::ClientMessage(event) => {
|
||||
@@ -524,7 +479,9 @@ impl X11Client {
|
||||
window.set_focused(false);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.focused_window = None;
|
||||
state.compose_state.reset();
|
||||
if let Some(compose_state) = state.compose_state.as_mut() {
|
||||
compose_state.reset();
|
||||
}
|
||||
state.pre_edit_text.take();
|
||||
drop(state);
|
||||
self.disable_ime();
|
||||
@@ -570,37 +527,42 @@ impl X11Client {
|
||||
if keysym.is_modifier_key() {
|
||||
return Some(());
|
||||
}
|
||||
state.compose_state.feed(keysym);
|
||||
match state.compose_state.status() {
|
||||
xkbc::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.ime_key = state.compose_state.utf8();
|
||||
keystroke.key =
|
||||
xkbc::keysym_get_name(state.compose_state.keysym().unwrap());
|
||||
}
|
||||
xkbc::Status::Composing => {
|
||||
state.pre_edit_text = state
|
||||
.compose_state
|
||||
.utf8()
|
||||
.or(crate::Keystroke::underlying_dead_key(keysym));
|
||||
let pre_edit = state.pre_edit_text.clone().unwrap_or(String::default());
|
||||
drop(state);
|
||||
window.handle_ime_preedit(pre_edit);
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
xkbc::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
drop(state);
|
||||
if let Some(pre_edit) = pre_edit {
|
||||
window.handle_ime_commit(pre_edit);
|
||||
if let Some(mut compose_state) = state.compose_state.take() {
|
||||
compose_state.feed(keysym);
|
||||
match compose_state.status() {
|
||||
xkbc::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.ime_key = compose_state.utf8();
|
||||
if let Some(keysym) = compose_state.keysym() {
|
||||
keystroke.key = xkbc::keysym_get_name(keysym);
|
||||
}
|
||||
}
|
||||
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
|
||||
window.handle_ime_preedit(current_key);
|
||||
xkbc::Status::Composing => {
|
||||
keystroke.ime_key = None;
|
||||
state.pre_edit_text = compose_state
|
||||
.utf8()
|
||||
.or(crate::Keystroke::underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
state.pre_edit_text.clone().unwrap_or(String::default());
|
||||
drop(state);
|
||||
window.handle_ime_preedit(pre_edit);
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
state = self.0.borrow_mut();
|
||||
state.compose_state.feed(keysym);
|
||||
xkbc::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
drop(state);
|
||||
if let Some(pre_edit) = pre_edit {
|
||||
window.handle_ime_commit(pre_edit);
|
||||
}
|
||||
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
|
||||
window.handle_ime_preedit(current_key);
|
||||
}
|
||||
state = self.0.borrow_mut();
|
||||
compose_state.feed(keysym);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
state.compose_state = Some(compose_state);
|
||||
}
|
||||
keystroke
|
||||
};
|
||||
@@ -649,7 +611,9 @@ impl X11Client {
|
||||
window.handle_ime_unmark();
|
||||
state = self.0.borrow_mut();
|
||||
} else if let Some(text) = state.pre_edit_text.take() {
|
||||
state.compose_state.reset();
|
||||
if let Some(compose_state) = state.compose_state.as_mut() {
|
||||
compose_state.reset();
|
||||
}
|
||||
drop(state);
|
||||
window.handle_ime_commit(text);
|
||||
state = self.0.borrow_mut();
|
||||
@@ -901,6 +865,39 @@ impl X11Client {
|
||||
drop(state);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn send_window_expose_events(
|
||||
&self,
|
||||
x_windows: impl IntoIterator<Item = xproto::Window>,
|
||||
) -> anyhow::Result<()> {
|
||||
let state = self.0.borrow_mut();
|
||||
|
||||
for x_window in x_windows.into_iter() {
|
||||
state
|
||||
.xcb_connection
|
||||
.send_event(
|
||||
false,
|
||||
x_window,
|
||||
xproto::EventMask::EXPOSURE,
|
||||
xproto::ExposeEvent {
|
||||
response_type: xproto::EXPOSE_EVENT,
|
||||
sequence: 0,
|
||||
window: x_window,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
count: 1,
|
||||
},
|
||||
)
|
||||
.context("failed to send ExposeEvent for window")?;
|
||||
}
|
||||
|
||||
state
|
||||
.xcb_connection
|
||||
.flush()
|
||||
.context("failed to flush XCB connection after sending ExposeEvent")
|
||||
}
|
||||
}
|
||||
|
||||
impl LinuxClient for X11Client {
|
||||
@@ -973,69 +970,8 @@ impl LinuxClient for X11Client {
|
||||
state.common.appearance,
|
||||
)?;
|
||||
|
||||
let screen_resources = state
|
||||
.xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = state
|
||||
.xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
|
||||
screen_resources
|
||||
.modes
|
||||
.iter()
|
||||
.find(|m| m.id == crtc_info.mode)
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
let refresh_event_token = state
|
||||
.loop_handle
|
||||
.insert_source(calloop::timer::Timer::immediate(), {
|
||||
let refresh_duration = mode_refresh_rate(mode);
|
||||
move |mut instant, (), client| {
|
||||
let state = client.0.borrow_mut();
|
||||
state
|
||||
.xcb_connection
|
||||
.send_event(
|
||||
false,
|
||||
x_window,
|
||||
xproto::EventMask::EXPOSURE,
|
||||
xproto::ExposeEvent {
|
||||
response_type: xproto::EXPOSE_EVENT,
|
||||
sequence: 0,
|
||||
window: x_window,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
count: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let _ = state.xcb_connection.flush().unwrap();
|
||||
// Take into account that some frames have been skipped
|
||||
let now = Instant::now();
|
||||
while instant < now {
|
||||
instant += refresh_duration;
|
||||
}
|
||||
calloop::timer::TimeoutAction::ToInstant(instant)
|
||||
}
|
||||
})
|
||||
.expect("Failed to initialize refresh timer");
|
||||
|
||||
let window_ref = WindowRef {
|
||||
window: window.0.clone(),
|
||||
refresh_event_token,
|
||||
};
|
||||
|
||||
state.windows.insert(x_window, window_ref);
|
||||
@@ -1097,7 +1033,7 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
let state = self.0.borrow_mut();
|
||||
let mut state = self.0.borrow_mut();
|
||||
state
|
||||
.clipboard
|
||||
.store(
|
||||
@@ -1106,6 +1042,7 @@ impl LinuxClient for X11Client {
|
||||
item.text().as_bytes(),
|
||||
)
|
||||
.ok();
|
||||
state.clipboard_item.replace(item);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
@@ -1127,6 +1064,20 @@ impl LinuxClient for X11Client {
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
let state = self.0.borrow_mut();
|
||||
// if the last copy was from this app, return our cached item
|
||||
// which has metadata attached.
|
||||
if state
|
||||
.clipboard
|
||||
.setter
|
||||
.connection
|
||||
.get_selection_owner(state.clipboard.setter.atoms.clipboard)
|
||||
.ok()
|
||||
.and_then(|r| r.reply().ok())
|
||||
.map(|reply| reply.owner == state.clipboard.setter.window)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return state.clipboard_item.clone();
|
||||
}
|
||||
state
|
||||
.clipboard
|
||||
.load(
|
||||
@@ -1143,14 +1094,108 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
|
||||
fn run(&self) {
|
||||
let mut event_loop = self
|
||||
.0
|
||||
.borrow_mut()
|
||||
.event_loop
|
||||
.take()
|
||||
.expect("App is already running");
|
||||
loop {
|
||||
{
|
||||
let mut state = self.0.borrow_mut();
|
||||
if let Ok(Some(())) = state.quit_signal_rx.try_recv() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||
// Send expose events to windows that need refreshing
|
||||
let mut windows_to_expose = HashSet::new();
|
||||
{
|
||||
let state = self.0.borrow_mut();
|
||||
for (x_window, window_ref) in state.windows.iter() {
|
||||
if window_ref.window.needs_refresh() {
|
||||
windows_to_expose.insert(*x_window);
|
||||
window_ref.window.set_refresh_queued(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut sleep = windows_to_expose.is_empty();
|
||||
let _ = self.send_window_expose_events(windows_to_expose).log_err();
|
||||
|
||||
// Read all X11 events and then handle them in a batch
|
||||
{
|
||||
let mut events = Vec::new();
|
||||
let mut windows_to_refresh = HashSet::new();
|
||||
|
||||
{
|
||||
let state = self.0.borrow_mut();
|
||||
while let Ok(Some(event)) = state.xcb_connection.poll_for_event() {
|
||||
if let Event::Expose(event) = event {
|
||||
windows_to_refresh.insert(event.window);
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep = !sleep && events.is_empty() && windows_to_refresh.is_empty();
|
||||
// We prioritize Expose events so that a lot of input events don't hold up
|
||||
// a render.
|
||||
for window in windows_to_refresh.into_iter() {
|
||||
if let Some(window) = self.get_window(window) {
|
||||
window.refresh();
|
||||
window.set_refresh_queued(false);
|
||||
}
|
||||
}
|
||||
|
||||
self.handle_events(events);
|
||||
}
|
||||
|
||||
// Handle runnables
|
||||
{
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let now = Instant::now();
|
||||
while let Ok(runnable) = state.runnables.try_recv() {
|
||||
drop(state);
|
||||
|
||||
runnable.run();
|
||||
|
||||
sleep = false;
|
||||
|
||||
if now.elapsed() >= Duration::from_millis(2) {
|
||||
println!("ran runnables for over 2ms");
|
||||
break;
|
||||
}
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle XDG events
|
||||
{
|
||||
let mut state = self.0.borrow_mut();
|
||||
while let Ok(event) = state.xdp_event_source.try_recv() {
|
||||
drop(state);
|
||||
|
||||
sleep = false;
|
||||
|
||||
match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
self.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut self.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
|
||||
// noop, X11 manages this for us.
|
||||
}
|
||||
};
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep for a very short duration to prevent busy-waiting
|
||||
// But only if we had nothing to do in this iteration.
|
||||
if sleep {
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
@@ -1164,15 +1209,6 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Adatpted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
|
||||
let micros = 1_000_000_000 / millihertz;
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
|
||||
value.integral as f32 + value.frac as f32 / u32::MAX as f32
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use util::{maybe, ResultExt};
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
protocol::{
|
||||
randr::{self, ConnectionExt as _},
|
||||
xinput::{self, ConnectionExt as _},
|
||||
xproto::{
|
||||
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
|
||||
@@ -31,10 +32,10 @@ use std::{
|
||||
ptr::NonNull,
|
||||
rc::Rc,
|
||||
sync::{self, Arc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use super::{X11Display, XINPUT_MASTER_DEVICE};
|
||||
|
||||
x11rb::atom_manager! {
|
||||
pub XcbAtoms: AtomsCookie {
|
||||
UTF8_STRING,
|
||||
@@ -159,6 +160,9 @@ pub struct Callbacks {
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub destroyed: bool,
|
||||
pub refresh_rate: Duration,
|
||||
refresh_queued: bool,
|
||||
pub last_refresh_at: Option<Instant>,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
@@ -178,7 +182,7 @@ pub(crate) struct X11WindowStatePtr {
|
||||
pub state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
x_window: xproto::Window,
|
||||
pub x_window: xproto::Window,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
@@ -397,6 +401,31 @@ impl X11WindowState {
|
||||
};
|
||||
xcb_connection.map_window(x_window).unwrap();
|
||||
|
||||
let screen_resources = xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
|
||||
screen_resources
|
||||
.modes
|
||||
.iter()
|
||||
.find(|m| m.id == crtc_info.mode)
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
let refresh_rate = mode_refresh_rate(&mode);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
executor,
|
||||
@@ -413,6 +442,9 @@ impl X11WindowState {
|
||||
appearance,
|
||||
handle,
|
||||
destroyed: false,
|
||||
refresh_rate,
|
||||
refresh_queued: false,
|
||||
last_refresh_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -582,6 +614,10 @@ impl X11WindowStatePtr {
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(ref mut fun) = cb.request_frame {
|
||||
fun();
|
||||
self.state
|
||||
.borrow_mut()
|
||||
.last_refresh_at
|
||||
.replace(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,6 +751,23 @@ impl X11WindowStatePtr {
|
||||
(fun)()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn needs_refresh(&self) -> bool {
|
||||
let state = self.state.borrow();
|
||||
|
||||
if state.refresh_queued {
|
||||
return false;
|
||||
}
|
||||
|
||||
let refresh_rate = state.refresh_rate;
|
||||
state.last_refresh_at.map_or(false, |last_refresh_at| {
|
||||
last_refresh_at.elapsed() >= refresh_rate
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_refresh_queued(&self, value: bool) {
|
||||
self.state.borrow_mut().refresh_queued = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
@@ -1028,3 +1081,16 @@ impl PlatformWindow for X11Window {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
|
||||
return Duration::from_millis(16);
|
||||
}
|
||||
|
||||
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
|
||||
let micros = 1_000_000_000 / millihertz;
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! This module uses the [ashpd] crate
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ashpd::desktop::settings::{ColorScheme, Settings};
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
|
||||
@@ -98,6 +99,12 @@ impl XDPEventSource {
|
||||
|
||||
Self { channel }
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> anyhow::Result<Event> {
|
||||
self.channel
|
||||
.try_recv()
|
||||
.map_err(|error| anyhow!("{}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for XDPEventSource {
|
||||
|
||||
@@ -344,6 +344,7 @@ struct MacWindowState {
|
||||
// Whether the next left-mouse click is also the focusing click.
|
||||
first_mouse: bool,
|
||||
fullscreen_restore_bounds: Bounds<Pixels>,
|
||||
ime_composing: bool,
|
||||
}
|
||||
|
||||
impl MacWindowState {
|
||||
@@ -504,6 +505,7 @@ impl MacWindow {
|
||||
focus,
|
||||
show,
|
||||
display_id,
|
||||
window_min_size,
|
||||
}: WindowParams,
|
||||
executor: ForegroundExecutor,
|
||||
renderer_context: renderer::Context,
|
||||
@@ -623,6 +625,7 @@ impl MacWindow {
|
||||
external_files_dragged: false,
|
||||
first_mouse: false,
|
||||
fullscreen_restore_bounds: Bounds::default(),
|
||||
ime_composing: false,
|
||||
})));
|
||||
|
||||
(*native_window).set_ivar(
|
||||
@@ -644,6 +647,13 @@ impl MacWindow {
|
||||
|
||||
native_window.setMovable_(is_movable as BOOL);
|
||||
|
||||
if let Some(window_min_size) = window_min_size {
|
||||
native_window.setContentMinSize_(NSSize {
|
||||
width: window_min_size.width.to_f64(),
|
||||
height: window_min_size.height.to_f64(),
|
||||
});
|
||||
}
|
||||
|
||||
if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) {
|
||||
native_window.setTitlebarAppearsTransparent_(YES);
|
||||
native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
|
||||
@@ -1234,6 +1244,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
let mut lock = window_state.lock();
|
||||
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
|
||||
let mut last_inserts = lock.last_ime_inputs.take().unwrap();
|
||||
let ime_composing = std::mem::take(&mut lock.ime_composing);
|
||||
|
||||
let mut callback = lock.event_callback.take();
|
||||
drop(lock);
|
||||
@@ -1248,7 +1259,8 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
let is_composing =
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some();
|
||||
.is_some()
|
||||
|| ime_composing;
|
||||
|
||||
if let Some((text, range)) = last_insert {
|
||||
if !is_composing {
|
||||
@@ -1660,9 +1672,13 @@ extern "C" fn first_rect_for_character_range(
|
||||
range: NSRange,
|
||||
_: id,
|
||||
) -> NSRect {
|
||||
let frame = unsafe {
|
||||
let window = get_window_state(this).lock().native_window;
|
||||
NSView::frame(window)
|
||||
let frame: NSRect = unsafe {
|
||||
let state = get_window_state(this);
|
||||
let lock = state.lock();
|
||||
let mut frame = NSWindow::frame(lock.native_window);
|
||||
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
|
||||
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
|
||||
frame
|
||||
};
|
||||
with_input_handler(this, |input_handler| {
|
||||
input_handler.bounds_for_range(range.to_range()?)
|
||||
@@ -1922,6 +1938,7 @@ fn send_to_input_handler(window: &Object, ime: ImeInput) {
|
||||
input_handler.replace_text_in_range(range, &text)
|
||||
}
|
||||
ImeInput::SetMarkedText(text, range, marked_range) => {
|
||||
lock.ime_composing = true;
|
||||
drop(lock);
|
||||
input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
|
||||
}
|
||||
|
||||
@@ -267,14 +267,8 @@ fn handle_syskeydown_msg(
|
||||
) -> Option<isize> {
|
||||
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
|
||||
// shortcuts.
|
||||
let Some(keystroke) = parse_syskeydown_msg_keystroke(wparam) else {
|
||||
return None;
|
||||
};
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return None;
|
||||
};
|
||||
drop(lock);
|
||||
let keystroke = parse_syskeydown_msg_keystroke(wparam)?;
|
||||
let mut func = state_ptr.state.borrow_mut().callbacks.input.take()?;
|
||||
let event = KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: lparam.0 & (0x1 << 30) > 0,
|
||||
@@ -292,14 +286,8 @@ fn handle_syskeydown_msg(
|
||||
fn handle_syskeyup_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
|
||||
// shortcuts.
|
||||
let Some(keystroke) = parse_syskeydown_msg_keystroke(wparam) else {
|
||||
return None;
|
||||
};
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return None;
|
||||
};
|
||||
drop(lock);
|
||||
let keystroke = parse_syskeydown_msg_keystroke(wparam)?;
|
||||
let mut func = state_ptr.state.borrow_mut().callbacks.input.take()?;
|
||||
let event = KeyUpEvent { keystroke };
|
||||
let result = if func(PlatformInput::KeyUp(event)).default_prevented {
|
||||
Some(0)
|
||||
@@ -614,35 +602,25 @@ fn handle_ime_composition(
|
||||
) -> Option<isize> {
|
||||
let mut ime_input = None;
|
||||
if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
|
||||
let Some((string, string_len)) = parse_ime_compostion_string(handle) else {
|
||||
return None;
|
||||
};
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let Some(mut input_handler) = lock.input_handler.take() else {
|
||||
return None;
|
||||
};
|
||||
drop(lock);
|
||||
input_handler.replace_and_mark_text_in_range(None, string.as_str(), Some(0..string_len));
|
||||
let (comp_string, string_len) = parse_ime_compostion_string(handle)?;
|
||||
let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?;
|
||||
input_handler.replace_and_mark_text_in_range(
|
||||
None,
|
||||
&comp_string,
|
||||
Some(string_len..string_len),
|
||||
);
|
||||
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
|
||||
ime_input = Some(string);
|
||||
ime_input = Some(comp_string);
|
||||
}
|
||||
if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 {
|
||||
let Some(ref comp_string) = ime_input else {
|
||||
return None;
|
||||
};
|
||||
let comp_string = &ime_input?;
|
||||
let caret_pos = retrieve_composition_cursor_position(handle);
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let Some(mut input_handler) = lock.input_handler.take() else {
|
||||
return None;
|
||||
};
|
||||
drop(lock);
|
||||
input_handler.replace_and_mark_text_in_range(None, comp_string, Some(0..caret_pos));
|
||||
let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?;
|
||||
input_handler.replace_and_mark_text_in_range(None, comp_string, Some(caret_pos..caret_pos));
|
||||
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 {
|
||||
let Some(comp_result) = parse_ime_compostion_result(handle) else {
|
||||
return None;
|
||||
};
|
||||
let comp_result = parse_ime_compostion_result(handle)?;
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let Some(mut input_handler) = lock.input_handler.take() else {
|
||||
return Some(1);
|
||||
@@ -663,11 +641,7 @@ fn handle_calc_client_size(
|
||||
lparam: LPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if wparam.0 == 0 {
|
||||
if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() || wparam.0 == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1097,13 +1071,14 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
|
||||
VK_NEXT => "pagedown",
|
||||
VK_ESCAPE => "escape",
|
||||
VK_INSERT => "insert",
|
||||
VK_DELETE => "delete",
|
||||
_ => return basic_vkcode_to_string(vk_code, modifiers),
|
||||
}
|
||||
.to_owned();
|
||||
|
||||
Some(Keystroke {
|
||||
modifiers,
|
||||
key: key,
|
||||
key,
|
||||
ime_key: None,
|
||||
})
|
||||
}
|
||||
@@ -1160,7 +1135,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
|
||||
|
||||
Some(KeystrokeOrModifier::Keystroke(Keystroke {
|
||||
modifiers,
|
||||
key: key,
|
||||
key,
|
||||
ime_key: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ impl<'de> serde::Deserialize<'de> for FontFeatures {
|
||||
while let Some((key, value)) =
|
||||
access.next_entry::<String, Option<FeatureValue>>()?
|
||||
{
|
||||
if key.len() != 4 && !key.is_ascii() {
|
||||
if !is_valid_feature_tag(&key) {
|
||||
log::error!("Incorrect font feature tag: {}", key);
|
||||
continue;
|
||||
}
|
||||
@@ -142,3 +142,7 @@ impl schemars::JsonSchema for FontFeatures {
|
||||
schema.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_feature_tag(tag: &str) -> bool {
|
||||
tag.len() == 4 && tag.chars().all(|c| c.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
@@ -549,6 +549,7 @@ pub struct Window {
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
focus_enabled: bool,
|
||||
pending_input: Option<PendingInput>,
|
||||
pending_modifiers: Option<Modifiers>,
|
||||
pending_input_observers: SubscriberSet<(), AnyObserver>,
|
||||
prompt: Option<RenderablePromptHandle>,
|
||||
}
|
||||
@@ -631,6 +632,7 @@ impl Window {
|
||||
display_id,
|
||||
window_background,
|
||||
app_id,
|
||||
window_min_size,
|
||||
} = options;
|
||||
|
||||
let bounds = window_bounds
|
||||
@@ -647,6 +649,7 @@ impl Window {
|
||||
show,
|
||||
display_id,
|
||||
window_background,
|
||||
window_min_size,
|
||||
},
|
||||
)?;
|
||||
let display_id = platform_window.display().map(|display| display.id());
|
||||
@@ -821,6 +824,7 @@ impl Window {
|
||||
focus: None,
|
||||
focus_enabled: true,
|
||||
pending_input: None,
|
||||
pending_modifiers: None,
|
||||
pending_input_observers: SubscriberSet::new(),
|
||||
prompt: None,
|
||||
})
|
||||
@@ -3159,70 +3163,129 @@ impl<'a> WindowContext<'a> {
|
||||
.dispatch_tree
|
||||
.dispatch_path(node_id);
|
||||
|
||||
let mut bindings: SmallVec<[KeyBinding; 1]> = SmallVec::new();
|
||||
let mut pending = false;
|
||||
let mut keystroke: Option<Keystroke> = None;
|
||||
|
||||
if let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() {
|
||||
if let Some(previous) = self.window.pending_modifiers.take() {
|
||||
if event.modifiers.number_of_modifiers() == 0 {
|
||||
let key = match previous {
|
||||
modifiers if modifiers.shift => Some("shift"),
|
||||
modifiers if modifiers.control => Some("control"),
|
||||
modifiers if modifiers.alt => Some("alt"),
|
||||
modifiers if modifiers.platform => Some("platform"),
|
||||
modifiers if modifiers.function => Some("function"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(key) = key {
|
||||
let key = Keystroke {
|
||||
key: key.to_string(),
|
||||
ime_key: None,
|
||||
modifiers: Modifiers::default(),
|
||||
};
|
||||
let KeymatchResult {
|
||||
bindings: modifier_bindings,
|
||||
pending: pending_bindings,
|
||||
} = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key, &dispatch_path);
|
||||
|
||||
keystroke = Some(key);
|
||||
bindings = modifier_bindings;
|
||||
pending = pending_bindings;
|
||||
}
|
||||
}
|
||||
} else if event.modifiers.number_of_modifiers() == 1 {
|
||||
self.window.pending_modifiers = Some(event.modifiers);
|
||||
}
|
||||
if keystroke.is_none() {
|
||||
self.finish_dispatch_key_event(event, dispatch_path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||
let KeymatchResult { bindings, pending } = self
|
||||
self.window.pending_modifiers.take();
|
||||
let KeymatchResult {
|
||||
bindings: key_down_bindings,
|
||||
pending: key_down_pending,
|
||||
} = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
|
||||
|
||||
if pending {
|
||||
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
|
||||
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
|
||||
{
|
||||
currently_pending = PendingInput::default();
|
||||
}
|
||||
currently_pending.focus = self.window.focus;
|
||||
currently_pending
|
||||
.keystrokes
|
||||
.push(key_down_event.keystroke.clone());
|
||||
for binding in bindings {
|
||||
currently_pending.bindings.push(binding);
|
||||
}
|
||||
keystroke = Some(key_down_event.keystroke.clone());
|
||||
|
||||
currently_pending.timer = Some(self.spawn(|mut cx| async move {
|
||||
cx.background_executor.timer(Duration::from_secs(1)).await;
|
||||
cx.update(move |cx| {
|
||||
cx.clear_pending_keystrokes();
|
||||
let Some(currently_pending) = cx.window.pending_input.take() else {
|
||||
return;
|
||||
};
|
||||
cx.pending_input_changed();
|
||||
cx.replay_pending_input(currently_pending);
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
bindings = key_down_bindings;
|
||||
pending = key_down_pending;
|
||||
}
|
||||
|
||||
self.window.pending_input = Some(currently_pending);
|
||||
self.pending_input_changed();
|
||||
|
||||
self.propagate_event = false;
|
||||
|
||||
return;
|
||||
} else if let Some(currently_pending) = self.window.pending_input.take() {
|
||||
self.pending_input_changed();
|
||||
if bindings
|
||||
.iter()
|
||||
.all(|binding| !currently_pending.used_by_binding(binding))
|
||||
{
|
||||
self.replay_pending_input(currently_pending)
|
||||
}
|
||||
if pending {
|
||||
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
|
||||
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus {
|
||||
currently_pending = PendingInput::default();
|
||||
}
|
||||
|
||||
if !bindings.is_empty() {
|
||||
self.clear_pending_keystrokes();
|
||||
currently_pending.focus = self.window.focus;
|
||||
if let Some(keystroke) = keystroke {
|
||||
currently_pending.keystrokes.push(keystroke.clone());
|
||||
}
|
||||
|
||||
self.propagate_event = true;
|
||||
for binding in bindings {
|
||||
self.dispatch_action_on_node(node_id, binding.action.as_ref());
|
||||
if !self.propagate_event {
|
||||
self.dispatch_keystroke_observers(event, Some(binding.action));
|
||||
return;
|
||||
}
|
||||
currently_pending.bindings.push(binding);
|
||||
}
|
||||
|
||||
currently_pending.timer = Some(self.spawn(|mut cx| async move {
|
||||
cx.background_executor.timer(Duration::from_secs(1)).await;
|
||||
cx.update(move |cx| {
|
||||
cx.clear_pending_keystrokes();
|
||||
let Some(currently_pending) = cx.window.pending_input.take() else {
|
||||
return;
|
||||
};
|
||||
cx.replay_pending_input(currently_pending);
|
||||
cx.pending_input_changed();
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
|
||||
self.window.pending_input = Some(currently_pending);
|
||||
self.pending_input_changed();
|
||||
|
||||
self.propagate_event = false;
|
||||
return;
|
||||
} else if let Some(currently_pending) = self.window.pending_input.take() {
|
||||
self.pending_input_changed();
|
||||
if bindings
|
||||
.iter()
|
||||
.all(|binding| !currently_pending.used_by_binding(binding))
|
||||
{
|
||||
self.replay_pending_input(currently_pending)
|
||||
}
|
||||
}
|
||||
|
||||
if !bindings.is_empty() {
|
||||
self.clear_pending_keystrokes();
|
||||
}
|
||||
|
||||
self.propagate_event = true;
|
||||
for binding in bindings {
|
||||
self.dispatch_action_on_node(node_id, binding.action.as_ref());
|
||||
if !self.propagate_event {
|
||||
self.dispatch_keystroke_observers(event, Some(binding.action));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.finish_dispatch_key_event(event, dispatch_path)
|
||||
}
|
||||
|
||||
fn finish_dispatch_key_event(
|
||||
&mut self,
|
||||
event: &dyn Any,
|
||||
dispatch_path: SmallVec<[DispatchNodeId; 32]>,
|
||||
) {
|
||||
self.dispatch_key_down_up_event(event, &dispatch_path);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
|
||||
@@ -511,7 +511,7 @@ impl LanguageRegistry {
|
||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||
let filename = path.file_name().and_then(|name| name.to_str());
|
||||
let extension = path.extension_or_hidden_file_name();
|
||||
let path_suffixes = [extension, filename];
|
||||
let path_suffixes = [extension, filename, path.to_str()];
|
||||
let empty = GlobSet::empty();
|
||||
|
||||
let rx = self.get_or_load_language(move |language_name, config| {
|
||||
|
||||
@@ -662,6 +662,17 @@ impl settings::Settings for AllLanguageSettings {
|
||||
.ok_or_else(Self::missing_default)?;
|
||||
|
||||
let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
|
||||
|
||||
for (language, suffixes) in &default_value.file_types {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
|
||||
for suffix in suffixes {
|
||||
builder.add(Glob::new(suffix)?);
|
||||
}
|
||||
|
||||
file_types.insert(language.clone(), builder.build()?);
|
||||
}
|
||||
|
||||
for user_settings in sources.customizations() {
|
||||
if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
|
||||
copilot_enabled = Some(copilot);
|
||||
@@ -701,6 +712,15 @@ impl settings::Settings for AllLanguageSettings {
|
||||
for (language, suffixes) in &user_settings.file_types {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
|
||||
let default_value = default_value.file_types.get(&language.clone());
|
||||
|
||||
// Merge the default value with the user's value.
|
||||
if let Some(suffixes) = default_value {
|
||||
for suffix in suffixes {
|
||||
builder.add(Glob::new(suffix)?);
|
||||
}
|
||||
}
|
||||
|
||||
for suffix in suffixes {
|
||||
builder.add(Glob::new(suffix)?);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, AsyncAppContext};
|
||||
use http::github::{latest_github_release, GitHubLspBinaryVersion};
|
||||
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use serde_json::{json, Value};
|
||||
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
|
||||
use smol::fs;
|
||||
use smol::{
|
||||
fs::{self},
|
||||
io::BufReader,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
env::consts,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::{maybe, ResultExt};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
|
||||
const SERVER_PATH: &str =
|
||||
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
|
||||
@@ -206,7 +213,12 @@ impl LspAdapter for JsonLspAdapter {
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<String, String> {
|
||||
[("JSON".into(), "jsonc".into())].into_iter().collect()
|
||||
[
|
||||
("JSON".into(), "json".into()),
|
||||
("JSONC".into(), "jsonc".into()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,3 +263,137 @@ fn schema_file_match(path: &Path) -> String {
|
||||
.to_string()
|
||||
.replace('\\', "/")
|
||||
}
|
||||
|
||||
pub(super) struct NodeVersionAdapter;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for NodeVersionAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("package-version-server".into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
let release = latest_github_release(
|
||||
"zed-industries/package-version-server",
|
||||
true,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
let os = match consts::OS {
|
||||
"macos" => "apple-darwin",
|
||||
"linux" => "unknown-linux-gnu",
|
||||
"windows" => "pc-windows-msvc",
|
||||
other => bail!("Running on unsupported os: {other}"),
|
||||
};
|
||||
let suffix = if consts::OS == "windows" {
|
||||
".zip"
|
||||
} else {
|
||||
".tar.gz"
|
||||
};
|
||||
let asset_name = format!("package-version-server-{}-{os}{suffix}", consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
|
||||
Ok(Box::new(GitHubLspBinaryVersion {
|
||||
name: release.tag_name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Box<dyn 'static + Send + Any>,
|
||||
container_dir: PathBuf,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = latest_version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||||
let destination_path =
|
||||
container_dir.join(format!("package-version-server-{}", version.name));
|
||||
let destination_container_path =
|
||||
container_dir.join(format!("package-version-server-{}-tmp", version.name));
|
||||
if fs::metadata(&destination_path).await.is_err() {
|
||||
let mut response = delegate
|
||||
.http_client()
|
||||
.get(&version.url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
if version.url.ends_with(".zip") {
|
||||
node_runtime::extract_zip(
|
||||
&destination_container_path,
|
||||
BufReader::new(response.body_mut()),
|
||||
)
|
||||
.await?;
|
||||
} else if version.url.ends_with(".tar.gz") {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(&destination_container_path).await?;
|
||||
}
|
||||
|
||||
fs::copy(
|
||||
destination_container_path.join("package-version-server"),
|
||||
&destination_path,
|
||||
)
|
||||
.await?;
|
||||
// todo("windows")
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
fs::set_permissions(
|
||||
&destination_path,
|
||||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
remove_matching(&container_dir, |entry| entry != destination_path).await;
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: destination_path,
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_delegate: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_version_server_binary(container_dir).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_version_server_binary(container_dir)
|
||||
.await
|
||||
.map(|mut binary| {
|
||||
binary.arguments = vec!["--version".into()];
|
||||
binary
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
|
||||
anyhow::Ok(LanguageServerBinary {
|
||||
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
3
crates/languages/src/jsonc/brackets.scm
Normal file
@@ -0,0 +1,3 @@
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
("\"" @open "\"" @close)
|
||||
12
crates/languages/src/jsonc/config.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
name = "JSONC"
|
||||
grammar = "jsonc"
|
||||
path_suffixes = ["jsonc"]
|
||||
line_comments = ["// "]
|
||||
autoclose_before = ",]}"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
tab_size = 2
|
||||
prettier_parser_name = "jsonc"
|
||||
14
crates/languages/src/jsonc/embedding.scm
Normal file
@@ -0,0 +1,14 @@
|
||||
; Only produce one embedding for the entire file.
|
||||
(document) @item
|
||||
|
||||
; Collapse arrays, except for the first object.
|
||||
(array
|
||||
"[" @keep
|
||||
.
|
||||
(object)? @keep
|
||||
"]" @keep) @collapse
|
||||
|
||||
; Collapse string values (but not keys).
|
||||
(pair value: (string
|
||||
"\"" @keep
|
||||
"\"" @keep) @collapse)
|
||||
21
crates/languages/src/jsonc/highlights.scm
Normal file
@@ -0,0 +1,21 @@
|
||||
(comment) @comment
|
||||
|
||||
(string) @string
|
||||
|
||||
(pair
|
||||
key: (string) @property.json_key)
|
||||
|
||||
(number) @number
|
||||
|
||||
[
|
||||
(true)
|
||||
(false)
|
||||
(null)
|
||||
] @constant
|
||||
|
||||
[
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
2
crates/languages/src/jsonc/indents.scm
Normal file
@@ -0,0 +1,2 @@
|
||||
(array "]" @end) @indent
|
||||
(object "}" @end) @indent
|
||||
2
crates/languages/src/jsonc/outline.scm
Normal file
@@ -0,0 +1,2 @@
|
||||
(pair
|
||||
key: (string (string_content) @name)) @item
|
||||
1
crates/languages/src/jsonc/overrides.scm
Normal file
@@ -0,0 +1 @@
|
||||
(string) @string
|
||||
4
crates/languages/src/jsonc/redactions.scm
Normal file
@@ -0,0 +1,4 @@
|
||||
(pair value: (number) @redact)
|
||||
(pair value: (string) @redact)
|
||||
(array (number) @redact)
|
||||
(array (string) @redact)
|
||||
@@ -45,6 +45,7 @@ pub fn init(
|
||||
("gowork", tree_sitter_gowork::language()),
|
||||
("jsdoc", tree_sitter_jsdoc::language()),
|
||||
("json", tree_sitter_json::language()),
|
||||
("jsonc", tree_sitter_json::language()),
|
||||
("markdown", tree_sitter_markdown::language()),
|
||||
("proto", tree_sitter_proto::language()),
|
||||
("python", tree_sitter_python::language()),
|
||||
@@ -117,6 +118,17 @@ pub fn init(
|
||||
|
||||
language!(
|
||||
"json",
|
||||
vec![
|
||||
Arc::new(json::JsonLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
languages.clone(),
|
||||
)),
|
||||
Arc::new(json::NodeVersionAdapter)
|
||||
],
|
||||
json_task_context()
|
||||
);
|
||||
language!(
|
||||
"jsonc",
|
||||
vec![Arc::new(json::JsonLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
languages.clone(),
|
||||
|
||||
@@ -201,11 +201,18 @@ impl LspAdapter for RustLspAdapter {
|
||||
completion: &lsp::CompletionItem,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let detail = completion
|
||||
.detail
|
||||
.as_ref()
|
||||
.or(completion
|
||||
.label_details
|
||||
.as_ref()
|
||||
.and_then(|detail| detail.detail.as_ref()))
|
||||
.map(ToOwned::to_owned);
|
||||
match completion.kind {
|
||||
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
|
||||
let detail = completion.detail.as_ref().unwrap();
|
||||
Some(lsp::CompletionItemKind::FIELD) if detail.is_some() => {
|
||||
let name = &completion.label;
|
||||
let text = format!("{}: {}", name, detail);
|
||||
let text = format!("{}: {}", name, detail.unwrap());
|
||||
let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
|
||||
let runs = language.highlight_text(&source, 11..11 + text.len());
|
||||
return Some(CodeLabel {
|
||||
@@ -215,12 +222,11 @@ impl LspAdapter for RustLspAdapter {
|
||||
});
|
||||
}
|
||||
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
|
||||
if completion.detail.is_some()
|
||||
if detail.is_some()
|
||||
&& completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
|
||||
{
|
||||
let detail = completion.detail.as_ref().unwrap();
|
||||
let name = &completion.label;
|
||||
let text = format!("{}: {}", name, detail);
|
||||
let text = format!("{}: {}", name, detail.unwrap());
|
||||
let source = Rope::from(format!("let {} = ();", text).as_str());
|
||||
let runs = language.highlight_text(&source, 4..4 + text.len());
|
||||
return Some(CodeLabel {
|
||||
@@ -230,12 +236,12 @@ impl LspAdapter for RustLspAdapter {
|
||||
});
|
||||
}
|
||||
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
|
||||
if completion.detail.is_some() =>
|
||||
if detail.is_some() =>
|
||||
{
|
||||
lazy_static! {
|
||||
static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
|
||||
}
|
||||
let detail = completion.detail.as_ref().unwrap();
|
||||
let detail = detail.unwrap();
|
||||
const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
|
||||
let prefix = FUNCTION_PREFIXES
|
||||
.iter()
|
||||
@@ -269,9 +275,14 @@ impl LspAdapter for RustLspAdapter {
|
||||
_ => None,
|
||||
};
|
||||
let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
|
||||
let mut label = CodeLabel::plain(completion.label.clone(), None);
|
||||
let mut label = completion.label.clone();
|
||||
if let Some(detail) = detail.filter(|detail| detail.starts_with(" (")) {
|
||||
use std::fmt::Write;
|
||||
write!(label, "{detail}").ok()?;
|
||||
}
|
||||
let mut label = CodeLabel::plain(label, None);
|
||||
label.runs.push((
|
||||
0..label.text.rfind('(').unwrap_or(label.text.len()),
|
||||
0..label.text.rfind('(').unwrap_or(completion.label.len()),
|
||||
highlight_id,
|
||||
));
|
||||
return Some(label);
|
||||
|
||||
@@ -200,6 +200,7 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"provideFormatter": true,
|
||||
"hostInfo": "zed",
|
||||
"tsserver": {
|
||||
"path": "node_modules/typescript/lib",
|
||||
},
|
||||
|
||||
@@ -162,47 +162,42 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"typescript":
|
||||
{
|
||||
"typescript": {
|
||||
"tsdk": "node_modules/typescript/lib",
|
||||
"format": {
|
||||
"enable": true
|
||||
},
|
||||
"inlayHints":{
|
||||
"parameterNames":
|
||||
{
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "all",
|
||||
"suppressWhenArgumentMatchesName": false,
|
||||
|
||||
},
|
||||
|
||||
"parameterTypes":
|
||||
{
|
||||
"parameterTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"variableTypes": {
|
||||
"enabled": true,
|
||||
"suppressWhenTypeMatchesName": false,
|
||||
},
|
||||
"propertyDeclarationTypes":{
|
||||
"propertyDeclarationTypes": {
|
||||
"enabled": true,
|
||||
},
|
||||
"functionLikeReturnTypes": {
|
||||
"enabled": true,
|
||||
},
|
||||
"enumMemberValues":{
|
||||
"enumMemberValues": {
|
||||
"enabled": true,
|
||||
}
|
||||
}
|
||||
},
|
||||
"vtsls":
|
||||
{"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
"vtsls": {
|
||||
"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -220,40 +215,36 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
"format": {
|
||||
"enable": true
|
||||
},
|
||||
"inlayHints":{
|
||||
"parameterNames":
|
||||
{
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "all",
|
||||
"suppressWhenArgumentMatchesName": false,
|
||||
|
||||
},
|
||||
|
||||
"parameterTypes":
|
||||
{
|
||||
"parameterTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"variableTypes": {
|
||||
"enabled": true,
|
||||
"suppressWhenTypeMatchesName": false,
|
||||
},
|
||||
"propertyDeclarationTypes":{
|
||||
"propertyDeclarationTypes": {
|
||||
"enabled": true,
|
||||
},
|
||||
"functionLikeReturnTypes": {
|
||||
"enabled": true,
|
||||
},
|
||||
"enumMemberValues":{
|
||||
"enumMemberValues": {
|
||||
"enabled": true,
|
||||
}
|
||||
}
|
||||
},
|
||||
"vtsls":
|
||||
{"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
}
|
||||
},
|
||||
"vtsls": {
|
||||
"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod archive;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
pub use archive::extract_zip;
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use futures::AsyncReadExt;
|
||||
|
||||
@@ -55,6 +55,8 @@ pub enum Model {
|
||||
#[serde(rename = "gpt-4o", alias = "gpt-4o-2024-05-13")]
|
||||
#[default]
|
||||
FourOmni,
|
||||
#[serde(rename = "custom")]
|
||||
Custom { name: String, max_tokens: usize },
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -74,15 +76,17 @@ impl Model {
|
||||
Self::Four => "gpt-4",
|
||||
Self::FourTurbo => "gpt-4-turbo-preview",
|
||||
Self::FourOmni => "gpt-4o",
|
||||
Self::Custom { .. } => "custom",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Self::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
Self::Four => "gpt-4",
|
||||
Self::FourTurbo => "gpt-4-turbo",
|
||||
Self::FourOmni => "gpt-4o",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +96,24 @@ impl Model {
|
||||
Model::Four => 8192,
|
||||
Model::FourTurbo => 128000,
|
||||
Model::FourOmni => 128000,
|
||||
Model::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_model<S>(model: &Model, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match model {
|
||||
Model::Custom { name, .. } => serializer.serialize_str(name),
|
||||
_ => serializer.serialize_str(model.id()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Request {
|
||||
#[serde(serialize_with = "serialize_model")]
|
||||
pub model: Model,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
|
||||
@@ -8192,7 +8192,7 @@ impl Project {
|
||||
}
|
||||
};
|
||||
|
||||
if abs_path.ends_with(local_settings_file_relative_path()) {
|
||||
if path.ends_with(local_settings_file_relative_path()) {
|
||||
let settings_dir = Arc::from(
|
||||
path.ancestors()
|
||||
.nth(local_settings_file_relative_path().components().count())
|
||||
@@ -8209,7 +8209,7 @@ impl Project {
|
||||
},
|
||||
)
|
||||
});
|
||||
} else if abs_path.ends_with(local_tasks_file_relative_path()) {
|
||||
} else if path.ends_with(local_tasks_file_relative_path()) {
|
||||
self.task_inventory().update(cx, |task_inventory, cx| {
|
||||
if removed {
|
||||
task_inventory.remove_local_static_source(&abs_path);
|
||||
@@ -8229,7 +8229,7 @@ impl Project {
|
||||
);
|
||||
}
|
||||
})
|
||||
} else if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
|
||||
} else if path.ends_with(local_vscode_tasks_file_relative_path()) {
|
||||
self.task_inventory().update(cx, |task_inventory, cx| {
|
||||
if removed {
|
||||
task_inventory.remove_local_static_source(&abs_path);
|
||||
|
||||
@@ -72,7 +72,7 @@ pub struct ProjectPanel {
|
||||
width: Option<Pixels>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
show_scrollbar: bool,
|
||||
is_dragging_scrollbar: Rc<Cell<bool>>,
|
||||
scrollbar_drag_thumb_offset: Rc<Cell<Option<f32>>>,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ impl ProjectPanel {
|
||||
pending_serialization: Task::ready(None),
|
||||
show_scrollbar: !Self::should_autohide_scrollbar(cx),
|
||||
hide_scrollbar_task: None,
|
||||
is_dragging_scrollbar: Default::default(),
|
||||
scrollbar_drag_thumb_offset: Default::default(),
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
|
||||
@@ -2231,7 +2231,7 @@ impl ProjectPanel {
|
||||
|
||||
let height = scroll_handle
|
||||
.last_item_height
|
||||
.filter(|_| self.show_scrollbar || self.is_dragging_scrollbar.get())?;
|
||||
.filter(|_| self.show_scrollbar || self.scrollbar_drag_thumb_offset.get().is_some())?;
|
||||
|
||||
let total_list_length = height.0 as f64 * items_count as f64;
|
||||
let current_offset = scroll_handle.base_handle.offset().y.0.min(0.).abs() as f64;
|
||||
@@ -2270,7 +2270,7 @@ impl ProjectPanel {
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, cx| {
|
||||
if !this.is_dragging_scrollbar.get()
|
||||
if this.scrollbar_drag_thumb_offset.get().is_none()
|
||||
&& !this.focus_handle.contains_focused(cx)
|
||||
{
|
||||
this.hide_scrollbar(cx);
|
||||
@@ -2293,7 +2293,7 @@ impl ProjectPanel {
|
||||
.child(ProjectPanelScrollbar::new(
|
||||
percentage as f32..end_offset as f32,
|
||||
self.scroll_handle.clone(),
|
||||
self.is_dragging_scrollbar.clone(),
|
||||
self.scrollbar_drag_thumb_offset.clone(),
|
||||
cx.view().clone().into(),
|
||||
items_count,
|
||||
)),
|
||||
|
||||
@@ -9,7 +9,8 @@ use ui::{prelude::*, px, relative, IntoElement};
|
||||
pub(crate) struct ProjectPanelScrollbar {
|
||||
thumb: Range<f32>,
|
||||
scroll: UniformListScrollHandle,
|
||||
is_dragging_scrollbar: Rc<Cell<bool>>,
|
||||
// If Some(), there's an active drag, offset by percentage from the top of thumb.
|
||||
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
|
||||
item_count: usize,
|
||||
view: AnyView,
|
||||
}
|
||||
@@ -18,14 +19,14 @@ impl ProjectPanelScrollbar {
|
||||
pub(crate) fn new(
|
||||
thumb: Range<f32>,
|
||||
scroll: UniformListScrollHandle,
|
||||
is_dragging_scrollbar: Rc<Cell<bool>>,
|
||||
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
|
||||
view: AnyView,
|
||||
item_count: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
thumb,
|
||||
scroll,
|
||||
is_dragging_scrollbar,
|
||||
scrollbar_drag_state,
|
||||
item_count,
|
||||
view,
|
||||
}
|
||||
@@ -97,7 +98,7 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
let item_count = self.item_count;
|
||||
cx.on_mouse_event({
|
||||
let scroll = self.scroll.clone();
|
||||
let is_dragging = self.is_dragging_scrollbar.clone();
|
||||
let is_dragging = self.scrollbar_drag_state.clone();
|
||||
move |event: &MouseDownEvent, phase, _cx| {
|
||||
if phase.bubble() && bounds.contains(&event.position) {
|
||||
if !thumb_bounds.contains(&event.position) {
|
||||
@@ -113,7 +114,9 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
.set_offset(point(px(0.), -max_offset * percentage));
|
||||
}
|
||||
} else {
|
||||
is_dragging.set(true);
|
||||
let thumb_top_offset =
|
||||
(event.position.y - thumb_bounds.origin.y) / bounds.size.height;
|
||||
is_dragging.set(Some(thumb_top_offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,14 +133,15 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
}
|
||||
}
|
||||
});
|
||||
let is_dragging = self.is_dragging_scrollbar.clone();
|
||||
let drag_state = self.scrollbar_drag_state.clone();
|
||||
let view_id = self.view.entity_id();
|
||||
cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
|
||||
if event.dragging() && is_dragging.get() {
|
||||
if let Some(drag_state) = drag_state.get().filter(|_| event.dragging()) {
|
||||
let scroll = scroll.0.borrow();
|
||||
if let Some(last_height) = scroll.last_item_height {
|
||||
let max_offset = item_count as f32 * last_height;
|
||||
let percentage = (event.position.y - bounds.origin.y) / bounds.size.height;
|
||||
let percentage =
|
||||
(event.position.y - bounds.origin.y) / bounds.size.height - drag_state;
|
||||
|
||||
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||
scroll
|
||||
@@ -146,13 +150,13 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
cx.notify(view_id);
|
||||
}
|
||||
} else {
|
||||
is_dragging.set(false);
|
||||
drag_state.set(None);
|
||||
}
|
||||
});
|
||||
let is_dragging = self.is_dragging_scrollbar.clone();
|
||||
let is_dragging = self.scrollbar_drag_state.clone();
|
||||
cx.on_mouse_event(move |_event: &MouseUpEvent, phase, cx| {
|
||||
if phase.bubble() {
|
||||
is_dragging.set(false);
|
||||
is_dragging.set(None);
|
||||
cx.notify(view_id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,7 +60,12 @@ impl ImageView {
|
||||
let bytes = base64::decode(base64_encoded_data)?;
|
||||
|
||||
let format = image::guess_format(&bytes)?;
|
||||
let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
|
||||
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
|
||||
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
|
||||
let height = data.height();
|
||||
let width = data.width();
|
||||
|
||||
@@ -31,6 +31,7 @@ pub enum ComponentStory {
|
||||
OverflowScroll,
|
||||
Picker,
|
||||
Scroll,
|
||||
Setting,
|
||||
Tab,
|
||||
TabBar,
|
||||
Text,
|
||||
@@ -64,6 +65,7 @@ impl ComponentStory {
|
||||
Self::ListItem => cx.new_view(|_| ui::ListItemStory).into(),
|
||||
Self::OverflowScroll => cx.new_view(|_| crate::stories::OverflowScrollStory).into(),
|
||||
Self::Scroll => ScrollStory::view(cx).into(),
|
||||
Self::Setting => cx.new_view(|cx| ui::SettingStory::init(cx)).into(),
|
||||
Self::Text => TextStory::view(cx).into(),
|
||||
Self::Tab => cx.new_view(|_| ui::TabStory).into(),
|
||||
Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(),
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{fmt::Display, sync::Arc, time::Duration};
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EventRequestBody {
|
||||
pub installation_id: Option<String>,
|
||||
pub metrics_id: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub is_staff: Option<bool>,
|
||||
pub app_version: String,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use gpui::{ClickEvent, WindowContext};
|
||||
use gpui::{ClickEvent, CursorStyle, WindowContext};
|
||||
|
||||
/// A trait for elements that can be clicked. Enables the use of the `on_click` method.
|
||||
pub trait Clickable {
|
||||
/// Sets the click handler that will fire whenever the element is clicked.
|
||||
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
|
||||
/// Sets the cursor style when hovering over the element.
|
||||
fn cursor_style(self, cursor_style: CursorStyle) -> Self;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ mod checkbox;
|
||||
mod context_menu;
|
||||
mod disclosure;
|
||||
mod divider;
|
||||
mod dropdown_menu;
|
||||
mod icon;
|
||||
mod indicator;
|
||||
mod keybinding;
|
||||
@@ -14,6 +15,7 @@ mod popover;
|
||||
mod popover_menu;
|
||||
mod radio;
|
||||
mod right_click_menu;
|
||||
mod setting;
|
||||
mod stack;
|
||||
mod tab;
|
||||
mod tab_bar;
|
||||
@@ -30,6 +32,7 @@ pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use divider::*;
|
||||
use dropdown_menu::*;
|
||||
pub use icon::*;
|
||||
pub use indicator::*;
|
||||
pub use keybinding::*;
|
||||
@@ -40,6 +43,7 @@ pub use popover::*;
|
||||
pub use popover_menu::*;
|
||||
pub use radio::*;
|
||||
pub use right_click_menu::*;
|
||||
pub use setting::*;
|
||||
pub use stack::*;
|
||||
pub use tab::*;
|
||||
pub use tab_bar::*;
|
||||
|
||||
@@ -249,6 +249,11 @@ impl Clickable for Button {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
|
||||
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
|
||||
self.base = self.base.cursor_style(cursor_style);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedWidth for Button {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use gpui::{relative, DefiniteLength, MouseButton};
|
||||
use gpui::{relative, CursorStyle, DefiniteLength, MouseButton};
|
||||
use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@@ -344,6 +344,7 @@ pub struct ButtonLike {
|
||||
size: ButtonSize,
|
||||
rounding: Option<ButtonLikeRounding>,
|
||||
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
|
||||
cursor_style: CursorStyle,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
@@ -363,6 +364,7 @@ impl ButtonLike {
|
||||
rounding: Some(ButtonLikeRounding::All),
|
||||
tooltip: None,
|
||||
children: SmallVec::new(),
|
||||
cursor_style: CursorStyle::PointingHand,
|
||||
on_click: None,
|
||||
layer: None,
|
||||
}
|
||||
@@ -405,6 +407,11 @@ impl Clickable for ButtonLike {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
|
||||
self.cursor_style = cursor_style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedWidth for ButtonLike {
|
||||
|
||||
@@ -86,6 +86,11 @@ impl Clickable for IconButton {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
|
||||
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
|
||||
self.base = self.base.cursor_style(cursor_style);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedWidth for IconButton {
|
||||
|
||||
@@ -82,6 +82,11 @@ impl Clickable for ToggleButton {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
|
||||
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
|
||||
self.base = self.base.cursor_style(cursor_style);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonCommon for ToggleButton {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::ClickEvent;
|
||||
use gpui::{ClickEvent, CursorStyle};
|
||||
|
||||
use crate::{prelude::*, Color, IconButton, IconButtonShape, IconName, IconSize};
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct Disclosure {
|
||||
is_open: bool,
|
||||
selected: bool,
|
||||
on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
cursor_style: CursorStyle,
|
||||
}
|
||||
|
||||
impl Disclosure {
|
||||
@@ -19,6 +20,7 @@ impl Disclosure {
|
||||
is_open,
|
||||
selected: false,
|
||||
on_toggle: None,
|
||||
cursor_style: CursorStyle::PointingHand,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +45,11 @@ impl Clickable for Disclosure {
|
||||
self.on_toggle = Some(Arc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
|
||||
self.cursor_style = cursor_style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Disclosure {
|
||||
|
||||
85
crates/ui/src/components/dropdown_menu.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// !!don't use this yet – it's not functional!!
|
||||
///
|
||||
/// pub crate until this is functional
|
||||
///
|
||||
/// just a placeholder for now for filling out the settings menu stories.
|
||||
#[derive(Debug, Clone, IntoElement)]
|
||||
pub(crate) struct DropdownMenu {
|
||||
pub id: ElementId,
|
||||
current_item: Option<SharedString>,
|
||||
// items: Vec<SharedString>,
|
||||
full_width: bool,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl DropdownMenu {
|
||||
pub fn new(id: impl Into<ElementId>, _cx: &WindowContext) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
current_item: None,
|
||||
// items: Vec::new(),
|
||||
full_width: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_item(mut self, current_item: Option<SharedString>) -> Self {
|
||||
self.current_item = current_item;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn full_width(mut self, full_width: bool) -> Self {
|
||||
self.full_width = full_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for DropdownMenu {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let disabled = self.disabled;
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.pl_2()
|
||||
.pr_1p5()
|
||||
.py_0p5()
|
||||
.gap_2()
|
||||
.min_w_20()
|
||||
.when_else(
|
||||
self.full_width,
|
||||
|full_width| full_width.w_full(),
|
||||
|auto_width| auto_width.flex_none().w_auto(),
|
||||
)
|
||||
.when_else(
|
||||
disabled,
|
||||
|disabled| disabled.cursor_not_allowed(),
|
||||
|enabled| enabled.cursor_pointer(),
|
||||
)
|
||||
.child(
|
||||
Label::new(self.current_item.unwrap_or("".into())).color(if disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Default
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronUpDown)
|
||||
.size(IconSize::XSmall)
|
||||
.color(if disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Muted
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -97,12 +97,16 @@ pub enum IconName {
|
||||
BellOff,
|
||||
BellRing,
|
||||
Bolt,
|
||||
Book,
|
||||
BookCopy,
|
||||
BookPlus,
|
||||
CaseSensitive,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
ChevronUpDown,
|
||||
Close,
|
||||
Code,
|
||||
Collab,
|
||||
@@ -138,6 +142,9 @@ pub enum IconName {
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderX,
|
||||
Font,
|
||||
FontSize,
|
||||
FontWeight,
|
||||
Github,
|
||||
Hash,
|
||||
HistoryRerun,
|
||||
@@ -145,6 +152,7 @@ pub enum IconName {
|
||||
IndicatorX,
|
||||
InlayHint,
|
||||
Library,
|
||||
LineHeight,
|
||||
Link,
|
||||
ListTree,
|
||||
MagicWand,
|
||||
@@ -178,8 +186,8 @@ pub enum IconName {
|
||||
RotateCw,
|
||||
Save,
|
||||
Screen,
|
||||
SelectAll,
|
||||
SearchSelection,
|
||||
SelectAll,
|
||||
Server,
|
||||
Settings,
|
||||
Shift,
|
||||
@@ -209,6 +217,7 @@ pub enum IconName {
|
||||
ZedAssistant,
|
||||
ZedAssistantFilled,
|
||||
ZedXCopilot,
|
||||
Visible,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
@@ -221,6 +230,7 @@ impl IconName {
|
||||
IconName::ArrowLeft => "icons/arrow_left.svg",
|
||||
IconName::ArrowRight => "icons/arrow_right.svg",
|
||||
IconName::ArrowUp => "icons/arrow_up.svg",
|
||||
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
|
||||
IconName::ArrowUpRight => "icons/arrow_up_right.svg",
|
||||
IconName::AtSign => "icons/at_sign.svg",
|
||||
IconName::AudioOff => "icons/speaker_off.svg",
|
||||
@@ -231,12 +241,16 @@ impl IconName {
|
||||
IconName::BellOff => "icons/bell_off.svg",
|
||||
IconName::BellRing => "icons/bell_ring.svg",
|
||||
IconName::Bolt => "icons/bolt.svg",
|
||||
IconName::Book => "icons/book.svg",
|
||||
IconName::BookCopy => "icons/book_copy.svg",
|
||||
IconName::BookPlus => "icons/book_plus.svg",
|
||||
IconName::CaseSensitive => "icons/case_insensitive.svg",
|
||||
IconName::Check => "icons/check.svg",
|
||||
IconName::ChevronDown => "icons/chevron_down.svg",
|
||||
IconName::ChevronLeft => "icons/chevron_left.svg",
|
||||
IconName::ChevronRight => "icons/chevron_right.svg",
|
||||
IconName::ChevronUp => "icons/chevron_up.svg",
|
||||
IconName::ChevronUpDown => "icons/chevron_up_down.svg",
|
||||
IconName::Close => "icons/x.svg",
|
||||
IconName::Code => "icons/code.svg",
|
||||
IconName::Collab => "icons/user_group_16.svg",
|
||||
@@ -272,6 +286,9 @@ impl IconName {
|
||||
IconName::Folder => "icons/file_icons/folder.svg",
|
||||
IconName::FolderOpen => "icons/file_icons/folder_open.svg",
|
||||
IconName::FolderX => "icons/stop_sharing.svg",
|
||||
IconName::Font => "icons/font.svg",
|
||||
IconName::FontSize => "icons/font_size.svg",
|
||||
IconName::FontWeight => "icons/font_weight.svg",
|
||||
IconName::Github => "icons/github.svg",
|
||||
IconName::Hash => "icons/hash.svg",
|
||||
IconName::HistoryRerun => "icons/history_rerun.svg",
|
||||
@@ -279,6 +296,7 @@ impl IconName {
|
||||
IconName::IndicatorX => "icons/indicator_x.svg",
|
||||
IconName::InlayHint => "icons/inlay_hint.svg",
|
||||
IconName::Library => "icons/library.svg",
|
||||
IconName::LineHeight => "icons/line_height.svg",
|
||||
IconName::Link => "icons/link.svg",
|
||||
IconName::ListTree => "icons/list_tree.svg",
|
||||
IconName::MagicWand => "icons/magic_wand.svg",
|
||||
@@ -302,18 +320,18 @@ impl IconName {
|
||||
IconName::Quote => "icons/quote.svg",
|
||||
IconName::Regex => "icons/regex.svg",
|
||||
IconName::Replace => "icons/replace.svg",
|
||||
IconName::Reveal => "icons/reveal.svg",
|
||||
IconName::ReplaceAll => "icons/replace_all.svg",
|
||||
IconName::ReplaceNext => "icons/replace_next.svg",
|
||||
IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
|
||||
IconName::Rerun => "icons/rerun.svg",
|
||||
IconName::Return => "icons/return.svg",
|
||||
IconName::RotateCw => "icons/rotate_cw.svg",
|
||||
IconName::Reveal => "icons/reveal.svg",
|
||||
IconName::RotateCcw => "icons/rotate_ccw.svg",
|
||||
IconName::RotateCw => "icons/rotate_cw.svg",
|
||||
IconName::Save => "icons/save.svg",
|
||||
IconName::Screen => "icons/desktop.svg",
|
||||
IconName::SelectAll => "icons/select_all.svg",
|
||||
IconName::SearchSelection => "icons/search_selection.svg",
|
||||
IconName::SelectAll => "icons/select_all.svg",
|
||||
IconName::Server => "icons/server.svg",
|
||||
IconName::Settings => "icons/file_icons/settings.svg",
|
||||
IconName::Shift => "icons/shift.svg",
|
||||
@@ -343,7 +361,7 @@ impl IconName {
|
||||
IconName::ZedAssistant => "icons/zed_assistant.svg",
|
||||
IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
|
||||
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
|
||||
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
|
||||
IconName::Visible => "icons/visible.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl KeyBinding {
|
||||
Some(Self::new(key_binding))
|
||||
}
|
||||
|
||||
fn icon_for_key(keystroke: &Keystroke) -> Option<IconName> {
|
||||
fn icon_for_key(&self, keystroke: &Keystroke) -> Option<IconName> {
|
||||
match keystroke.key.as_str() {
|
||||
"left" => Some(IconName::ArrowLeft),
|
||||
"right" => Some(IconName::ArrowRight),
|
||||
@@ -45,6 +45,11 @@ impl KeyBinding {
|
||||
"escape" => Some(IconName::Escape),
|
||||
"pagedown" => Some(IconName::PageDown),
|
||||
"pageup" => Some(IconName::PageUp),
|
||||
"shift" if self.platform_style == PlatformStyle::Mac => Some(IconName::Shift),
|
||||
"control" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control),
|
||||
"platform" if self.platform_style == PlatformStyle::Mac => Some(IconName::Command),
|
||||
"function" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control),
|
||||
"alt" if self.platform_style == PlatformStyle::Mac => Some(IconName::Option),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -80,7 +85,7 @@ impl RenderOnce for KeyBinding {
|
||||
.gap(Spacing::Small.rems(cx))
|
||||
.flex_none()
|
||||
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
||||
let key_icon = Self::icon_for_key(keystroke);
|
||||
let key_icon = self.icon_for_key(keystroke);
|
||||
|
||||
h_flex()
|
||||
.flex_none()
|
||||
|
||||
351
crates/ui/src/components/setting.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
use crate::{prelude::*, Checkbox, ListHeader};
|
||||
|
||||
use super::DropdownMenu;
|
||||
|
||||
#[derive(PartialEq, Clone, Eq, Debug)]
|
||||
pub enum ToggleType {
|
||||
Checkbox,
|
||||
// Switch,
|
||||
}
|
||||
|
||||
impl From<ToggleType> for SettingType {
|
||||
fn from(toggle_type: ToggleType) -> Self {
|
||||
SettingType::Toggle(toggle_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InputType {
|
||||
Text,
|
||||
Number,
|
||||
}
|
||||
|
||||
impl From<InputType> for SettingType {
|
||||
fn from(input_type: InputType) -> Self {
|
||||
SettingType::Input(input_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SecondarySettingType {
|
||||
Dropdown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SettingType {
|
||||
Toggle(ToggleType),
|
||||
ToggleAnd(SecondarySettingType),
|
||||
Input(InputType),
|
||||
Dropdown,
|
||||
Range,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoElement)]
|
||||
pub struct SettingsGroup {
|
||||
pub name: String,
|
||||
settings: Vec<SettingsItem>,
|
||||
}
|
||||
|
||||
impl SettingsGroup {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
settings: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_setting(mut self, setting: SettingsItem) -> Self {
|
||||
self.settings.push(setting);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SettingsGroup {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let empty_message = format!("No settings available for {}", self.name);
|
||||
|
||||
let header = ListHeader::new(self.name);
|
||||
|
||||
let settings = self.settings.clone().into_iter();
|
||||
|
||||
v_flex()
|
||||
.p_1()
|
||||
.gap_2()
|
||||
.child(header)
|
||||
.when(self.settings.len() == 0, |this| {
|
||||
this.child(Label::new(empty_message))
|
||||
})
|
||||
.children(settings)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SettingLayout {
|
||||
Stacked,
|
||||
AutoWidth,
|
||||
FullLine,
|
||||
FullLineJustified,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SettingId(pub SharedString);
|
||||
|
||||
impl From<SettingId> for ElementId {
|
||||
fn from(id: SettingId) -> Self {
|
||||
ElementId::Name(id.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SettingId {
|
||||
fn from(id: &str) -> Self {
|
||||
Self(id.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for SettingId {
|
||||
fn from(id: SharedString) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SettingValue(pub SharedString);
|
||||
|
||||
impl From<SharedString> for SettingValue {
|
||||
fn from(value: SharedString) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SettingValue {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for SettingValue {
|
||||
fn from(value: bool) -> Self {
|
||||
Self(value.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SettingValue> for bool {
|
||||
fn from(value: SettingValue) -> Self {
|
||||
value.0 == "true"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoElement)]
|
||||
pub struct SettingsItem {
|
||||
pub id: SettingId,
|
||||
current_value: Option<SettingValue>,
|
||||
disabled: bool,
|
||||
hide_label: bool,
|
||||
icon: Option<IconName>,
|
||||
layout: SettingLayout,
|
||||
name: SharedString,
|
||||
// possible_values: Option<Vec<SettingValue>>,
|
||||
setting_type: SettingType,
|
||||
toggled: Option<bool>,
|
||||
}
|
||||
|
||||
impl SettingsItem {
|
||||
pub fn new(
|
||||
id: impl Into<SettingId>,
|
||||
name: SharedString,
|
||||
setting_type: SettingType,
|
||||
current_value: Option<SettingValue>,
|
||||
) -> Self {
|
||||
let toggled = match setting_type {
|
||||
SettingType::Toggle(_) | SettingType::ToggleAnd(_) => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
current_value,
|
||||
disabled: false,
|
||||
hide_label: false,
|
||||
icon: None,
|
||||
layout: SettingLayout::FullLine,
|
||||
name,
|
||||
// possible_values: None,
|
||||
setting_type,
|
||||
toggled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout(mut self, layout: SettingLayout) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggled(mut self, toggled: bool) -> Self {
|
||||
self.toggled = Some(toggled);
|
||||
self
|
||||
}
|
||||
|
||||
// pub fn hide_label(mut self, hide_label: bool) -> Self {
|
||||
// self.hide_label = hide_label;
|
||||
// self
|
||||
// }
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
// pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
// self.disabled = disabled;
|
||||
// self
|
||||
// }
|
||||
}
|
||||
|
||||
impl RenderOnce for SettingsItem {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let id: ElementId = self.id.clone().into();
|
||||
|
||||
// When the setting is disabled or toggled off, we don't want any secondary elements to be interactable
|
||||
let secondary_element_disabled = self.disabled || self.toggled == Some(false);
|
||||
|
||||
let full_width = match self.layout {
|
||||
SettingLayout::FullLine | SettingLayout::FullLineJustified => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let hide_label = self.hide_label || self.icon.is_some();
|
||||
|
||||
let justified = match (self.layout.clone(), self.setting_type.clone()) {
|
||||
(_, SettingType::ToggleAnd(_)) => true,
|
||||
(SettingLayout::FullLineJustified, _) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let (setting_type, current_value) = (self.setting_type.clone(), self.current_value.clone());
|
||||
let current_string = if let Some(current_value) = current_value.clone() {
|
||||
Some(current_value.0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let toggleable = match setting_type {
|
||||
SettingType::Toggle(_) => true,
|
||||
SettingType::ToggleAnd(_) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let setting_element = match setting_type {
|
||||
SettingType::Toggle(_) => None,
|
||||
SettingType::ToggleAnd(secondary_setting_type) => match secondary_setting_type {
|
||||
SecondarySettingType::Dropdown => Some(
|
||||
DropdownMenu::new(id.clone(), &cx)
|
||||
.current_item(current_string)
|
||||
.disabled(secondary_element_disabled)
|
||||
.into_any_element(),
|
||||
),
|
||||
},
|
||||
SettingType::Input(input_type) => match input_type {
|
||||
InputType::Text => Some(div().child("text").into_any_element()),
|
||||
InputType::Number => Some(div().child("number").into_any_element()),
|
||||
},
|
||||
SettingType::Dropdown => Some(
|
||||
DropdownMenu::new(id.clone(), &cx)
|
||||
.current_item(current_string)
|
||||
.full_width(true)
|
||||
.into_any_element(),
|
||||
),
|
||||
SettingType::Range => Some(div().child("range").into_any_element()),
|
||||
SettingType::Unsupported => None,
|
||||
};
|
||||
|
||||
let checkbox = Checkbox::new(
|
||||
ElementId::Name(format!("toggle-{}", self.id.0).to_string().into()),
|
||||
self.toggled.into(),
|
||||
)
|
||||
.disabled(self.disabled);
|
||||
|
||||
let toggle_element = match (toggleable, self.setting_type.clone()) {
|
||||
(true, SettingType::Toggle(toggle_type)) => match toggle_type {
|
||||
ToggleType::Checkbox => Some(checkbox.into_any_element()),
|
||||
},
|
||||
(true, SettingType::ToggleAnd(_)) => Some(checkbox.into_any_element()),
|
||||
(_, _) => None,
|
||||
};
|
||||
|
||||
let item = if self.layout == SettingLayout::Stacked {
|
||||
v_flex()
|
||||
} else {
|
||||
h_flex()
|
||||
};
|
||||
|
||||
item.id(id)
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(div().px_0p5().child(Icon::new(icon).color(Color::Muted)))
|
||||
})
|
||||
.children(toggle_element)
|
||||
.children(if hide_label {
|
||||
None
|
||||
} else {
|
||||
Some(Label::new(self.name.clone()))
|
||||
})
|
||||
.when(justified, |this| this.child(div().flex_1().size_full()))
|
||||
.child(
|
||||
h_flex()
|
||||
.when(full_width, |this| this.w_full())
|
||||
.when(self.layout == SettingLayout::FullLineJustified, |this| {
|
||||
this.justify_end()
|
||||
})
|
||||
.children(setting_element),
|
||||
)
|
||||
// help flex along when full width is disabled
|
||||
//
|
||||
// this probably isn't needed, but fighting with flex to
|
||||
// get this right without inspection tools will be a pain
|
||||
.when(!full_width, |this| this.child(div().size_full().flex_1()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsMenu {
|
||||
name: SharedString,
|
||||
groups: Vec<SettingsGroup>,
|
||||
}
|
||||
|
||||
impl SettingsMenu {
|
||||
pub fn new(name: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_group(mut self, group: SettingsGroup) -> Self {
|
||||
self.groups.push(group);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SettingsMenu {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let is_empty = self.groups.is_empty();
|
||||
v_flex()
|
||||
.id(ElementId::Name(self.name.clone()))
|
||||
.elevation_2(cx)
|
||||
.min_w_56()
|
||||
.max_w_96()
|
||||
.max_h_2_3()
|
||||
.px_2()
|
||||
.when_else(
|
||||
is_empty,
|
||||
|empty| empty.py_1(),
|
||||
|not_empty| not_empty.pt_0().pb_1(),
|
||||
)
|
||||
.gap_1()
|
||||
.when(is_empty, |this| {
|
||||
this.child(Label::new("No settings found").color(Color::Muted))
|
||||
})
|
||||
.children(self.groups.clone())
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod label;
|
||||
mod list;
|
||||
mod list_header;
|
||||
mod list_item;
|
||||
mod setting;
|
||||
mod tab;
|
||||
mod tab_bar;
|
||||
mod title_bar;
|
||||
@@ -28,6 +29,7 @@ pub use label::*;
|
||||
pub use list::*;
|
||||
pub use list_header::*;
|
||||
pub use list_item::*;
|
||||
pub use setting::*;
|
||||
pub use tab::*;
|
||||
pub use tab_bar::*;
|
||||
pub use title_bar::*;
|
||||
|
||||
225
crates/ui/src/components/stories/setting.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use gpui::View;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::{
|
||||
SecondarySettingType, SettingLayout, SettingType, SettingsGroup, SettingsItem, SettingsMenu,
|
||||
ToggleType,
|
||||
};
|
||||
|
||||
pub struct SettingStory {
|
||||
menus: Vec<(SharedString, View<SettingsMenu>)>,
|
||||
}
|
||||
|
||||
impl SettingStory {
|
||||
pub fn new() -> Self {
|
||||
Self { menus: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut ViewContext<Self>) -> Self {
|
||||
let mut story = Self::new();
|
||||
story.empty_menu(cx);
|
||||
story.editor_example(cx);
|
||||
story.menu_single_group(cx);
|
||||
story
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingStory {
|
||||
pub fn empty_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let menu = cx.new_view(|_cx| SettingsMenu::new("Empty Menu"));
|
||||
|
||||
self.menus.push(("Empty Menu".into(), menu));
|
||||
}
|
||||
|
||||
pub fn menu_single_group(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let theme_setting = SettingsItem::new(
|
||||
"theme-setting",
|
||||
"Theme".into(),
|
||||
SettingType::Dropdown,
|
||||
Some(cx.theme().name.clone().into()),
|
||||
)
|
||||
.layout(SettingLayout::Stacked);
|
||||
let high_contrast_setting = SettingsItem::new(
|
||||
"theme-contrast",
|
||||
"Use high contrast theme".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(false);
|
||||
let appearance_setting = SettingsItem::new(
|
||||
"switch-appearance",
|
||||
"Match system appearance".into(),
|
||||
SettingType::ToggleAnd(SecondarySettingType::Dropdown),
|
||||
Some("When Dark".to_string().into()),
|
||||
)
|
||||
.layout(SettingLayout::FullLineJustified);
|
||||
|
||||
let group = SettingsGroup::new("Appearance")
|
||||
.add_setting(theme_setting)
|
||||
.add_setting(appearance_setting)
|
||||
.add_setting(high_contrast_setting);
|
||||
|
||||
let menu = cx.new_view(|_cx| SettingsMenu::new("Appearance").add_group(group));
|
||||
|
||||
self.menus.push(("Single Group".into(), menu));
|
||||
}
|
||||
|
||||
pub fn editor_example(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let font_group = SettingsGroup::new("Font")
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"font-family",
|
||||
"Font".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("Berkeley Mono".to_string().into()),
|
||||
)
|
||||
.icon(IconName::Font)
|
||||
.layout(SettingLayout::AutoWidth),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"font-weifht",
|
||||
"Font Weight".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("400".to_string().into()),
|
||||
)
|
||||
.icon(IconName::FontWeight)
|
||||
.layout(SettingLayout::AutoWidth),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"font-size",
|
||||
"Font Size".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("14".to_string().into()),
|
||||
)
|
||||
.icon(IconName::FontSize)
|
||||
.layout(SettingLayout::AutoWidth),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"line-height",
|
||||
"Line Height".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("1.35".to_string().into()),
|
||||
)
|
||||
.icon(IconName::LineHeight)
|
||||
.layout(SettingLayout::AutoWidth),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"enable-ligatures",
|
||||
"Enable Ligatures".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
);
|
||||
|
||||
let editor_group = SettingsGroup::new("Editor")
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-indent-guides",
|
||||
"Indent Guides".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-git-blame",
|
||||
"Git Blame".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(false),
|
||||
);
|
||||
|
||||
let gutter_group = SettingsGroup::new("Gutter")
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"enable-git-hunks",
|
||||
"Show Git Hunks".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-line-numbers",
|
||||
"Line Numbers".into(),
|
||||
SettingType::ToggleAnd(SecondarySettingType::Dropdown),
|
||||
Some("Ascending".to_string().into()),
|
||||
)
|
||||
.toggled(true)
|
||||
.layout(SettingLayout::FullLineJustified),
|
||||
);
|
||||
|
||||
let scrollbar_group = SettingsGroup::new("Scrollbar")
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"scrollbar-visibility",
|
||||
"Show scrollbar when:".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("Always Visible".to_string().into()),
|
||||
)
|
||||
.layout(SettingLayout::AutoWidth)
|
||||
.icon(IconName::Visible),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-diagnostic-markers",
|
||||
"Diagnostic Markers".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-git-markers",
|
||||
"Git Status Markers".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(false),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-selection-markers",
|
||||
"Selection & Match Markers".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
);
|
||||
|
||||
let menu = cx.new_view(|_cx| {
|
||||
SettingsMenu::new("Editor")
|
||||
.add_group(font_group)
|
||||
.add_group(editor_group)
|
||||
.add_group(gutter_group)
|
||||
.add_group(scrollbar_group)
|
||||
});
|
||||
|
||||
self.menus.push(("Editor Example".into(), menu));
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SettingStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.bg(cx.theme().colors().background)
|
||||
.text_color(cx.theme().colors().text)
|
||||
.children(self.menus.iter().map(|(name, menu)| {
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(Headline::new(name.clone()).size(HeadlineSize::Medium))
|
||||
.child(menu.clone())
|
||||
}))
|
||||
}
|
||||
}
|
||||