Compare commits
114 Commits
protobuf_f
...
tool-call-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6724d598e | ||
|
|
61a516e95f | ||
|
|
eb1754a091 | ||
|
|
2386595de5 | ||
|
|
b36ed56443 | ||
|
|
1b72c5402d | ||
|
|
a143fdc630 | ||
|
|
1e9666649e | ||
|
|
3c57a4071c | ||
|
|
ad6a07e574 | ||
|
|
c2668bc953 | ||
|
|
705a06c3dd | ||
|
|
f77b6ab79c | ||
|
|
ea5131ce0a | ||
|
|
1c2b3ad782 | ||
|
|
496dae968b | ||
|
|
5c6565a9e0 | ||
|
|
7853e32f80 | ||
|
|
f5cbfa718e | ||
|
|
6a2c712990 | ||
|
|
9454f0f1c7 | ||
|
|
5b0c15d8c4 | ||
|
|
aae39071ef | ||
|
|
a35b73e63e | ||
|
|
c0d11be75f | ||
|
|
0e26d22fea | ||
|
|
bd0f197415 | ||
|
|
343c88574a | ||
|
|
e7a0890086 | ||
|
|
d4c5c0f05e | ||
|
|
f0c7e62adc | ||
|
|
80d50f56f3 | ||
|
|
fb6c987e3e | ||
|
|
b4c2f29c8b | ||
|
|
8666ec95ba | ||
|
|
889aac9c03 | ||
|
|
5b9916e34b | ||
|
|
5b317f60df | ||
|
|
e2552b9add | ||
|
|
37899187c6 | ||
|
|
d265e44209 | ||
|
|
f12981db32 | ||
|
|
d99f5fe83e | ||
|
|
df1d0dec0a | ||
|
|
ad94ad511a | ||
|
|
0e7770a9a2 | ||
|
|
3f905d57e5 | ||
|
|
f01a86c644 | ||
|
|
5fd7afb9da | ||
|
|
9260abafba | ||
|
|
d92166f9f6 | ||
|
|
59a355da74 | ||
|
|
ee207ab77e | ||
|
|
31566cb5a0 | ||
|
|
2d3476530e | ||
|
|
f9990b42fa | ||
|
|
97e9137cb7 | ||
|
|
932c7e23c8 | ||
|
|
65a9c8d994 | ||
|
|
33f09bad60 | ||
|
|
792c1e4710 | ||
|
|
b421ffafb5 | ||
|
|
21c785ede4 | ||
|
|
516f7b3642 | ||
|
|
f34877334e | ||
|
|
6e296eb4b6 | ||
|
|
4c8c6c08fe | ||
|
|
050ce919ba | ||
|
|
369828f51c | ||
|
|
ac5ecf5487 | ||
|
|
1235d0808e | ||
|
|
6ff69faf37 | ||
|
|
f449e8d3d3 | ||
|
|
da09cbd055 | ||
|
|
4327459d2a | ||
|
|
cc601bd770 | ||
|
|
c491b75e07 | ||
|
|
3420ebb428 | ||
|
|
b23d72ec4f | ||
|
|
e25a03cd3c | ||
|
|
9e8ff3f198 | ||
|
|
6d80d5b74b | ||
|
|
7137bdee02 | ||
|
|
98403aa994 | ||
|
|
794ad1af75 | ||
|
|
4b1f0c033b | ||
|
|
3796b4a55c | ||
|
|
8c02929710 | ||
|
|
1e14697bb6 | ||
|
|
f619a872b5 | ||
|
|
c03f5b351b | ||
|
|
a8df0642a8 | ||
|
|
aee01f2c50 | ||
|
|
c9546070ac | ||
|
|
1855a312d0 | ||
|
|
332b33716a | ||
|
|
acf25324be | ||
|
|
f0882f44a7 | ||
|
|
189a034e71 | ||
|
|
7f52071513 | ||
|
|
56c93be4de | ||
|
|
43999c47e1 | ||
|
|
690a725667 | ||
|
|
b5ce8e7aa5 | ||
|
|
d177a1d4e5 | ||
|
|
5d17cfab31 | ||
|
|
404ddeebc5 | ||
|
|
ad370ed986 | ||
|
|
ced9045591 | ||
|
|
0d9bcbba25 | ||
|
|
c650ba4e72 | ||
|
|
5fab3ca5ba | ||
|
|
621a200d2f | ||
|
|
2544fad8a4 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -244,6 +244,7 @@ jobs:
|
||||
#
|
||||
# 25 was chosen arbitrarily.
|
||||
fetch-depth: 25
|
||||
fetch-tags: true
|
||||
clean: false
|
||||
|
||||
- name: Limit target directory size
|
||||
@@ -261,6 +262,9 @@ jobs:
|
||||
mkdir -p target/
|
||||
# Ignore any errors that occur while drafting release notes to not fail the build.
|
||||
script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true
|
||||
script/create-draft-release target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
@@ -306,7 +310,6 @@ jobs:
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
body_path: target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -353,7 +356,6 @@ jobs:
|
||||
files: |
|
||||
target/zed-remote-server-linux-x86_64.gz
|
||||
target/release/zed-linux-x86_64.tar.gz
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -400,6 +402,5 @@ jobs:
|
||||
files: |
|
||||
target/zed-remote-server-linux-aarch64.gz
|
||||
target/release/zed-linux-aarch64.tar.gz
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
21
.github/workflows/script_checks.yml
vendored
Normal file
21
.github/workflows/script_checks.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Script
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "script/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
name: "ShellCheck Scripts"
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Shellcheck ./scripts
|
||||
run: |
|
||||
./script/shellcheck-scripts error
|
||||
12
.mailmap
12
.mailmap
@@ -22,10 +22,14 @@ Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
|
||||
Bennet Bo Fenner <bennet@zed.dev>
|
||||
Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
|
||||
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
|
||||
Boris Cherny <boris@anthropic.com>
|
||||
Boris Cherny <boris@anthropic.com> <boris@performancejs.com>
|
||||
Chris Hayes <chris+git@hayes.software>
|
||||
Christian Bergschneider <christian.bergschneider@gmx.de>
|
||||
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
|
||||
Conrad Irwin <conrad@zed.dev>
|
||||
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
|
||||
Dairon Medina <dairon.medina@gmail.com>
|
||||
Danilo Leal <danilo@zed.dev>
|
||||
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
|
||||
Evren Sen <nervenes@icloud.com>
|
||||
@@ -35,6 +39,7 @@ Fernando Tagawa <tagawafernando@gmail.com>
|
||||
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
|
||||
Greg Morenz <greg-morenz@droid.cafe>
|
||||
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
|
||||
Ihnat Aŭtuška <autushka.ihnat@gmail.com>
|
||||
Ivan Žužak <izuzak@gmail.com>
|
||||
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
|
||||
Joseph T. Lyons <JosephTLyons@gmail.com>
|
||||
@@ -61,10 +66,13 @@ Max Brunsfeld <maxbrunsfeld@gmail.com> <max@zed.dev>
|
||||
Max Linke <maxlinke88@gmail.com>
|
||||
Max Linke <maxlinke88@gmail.com> <kain88-de@users.noreply.github.com>
|
||||
Michael Sloan <michael@zed.dev>
|
||||
Michael Sloan <michael@zed.dev> <mgsloan@gmail.com>
|
||||
Michael Sloan <michael@zed.dev> <mgsloan@google.com>
|
||||
Mikayla Maki <mikayla@zed.dev>
|
||||
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
|
||||
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
|
||||
Muhammad Talal Anwar <mail@talal.io>
|
||||
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
|
||||
Nate Butler <iamnbutler@gmail.com>
|
||||
Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
|
||||
Nathan Sobo <nathan@zed.dev>
|
||||
@@ -88,7 +96,11 @@ Robert Clover <git@clo4.net>
|
||||
Robert Clover <git@clo4.net> <robert@clover.gdn>
|
||||
Roy Williams <roy.williams.iii@gmail.com>
|
||||
Roy Williams <roy.williams.iii@gmail.com> <roy@anthropic.com>
|
||||
Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev>
|
||||
Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev> <sebastijan.kelneric@vichava.com>
|
||||
Sergey Onufrienko <sergey@onufrienko.com>
|
||||
Shish <webmaster@shishnet.org>
|
||||
Shish <webmaster@shishnet.org> <shish@shishnet.org>
|
||||
Thorben Kröger <dev@thorben.net>
|
||||
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
|
||||
Thorsten Ball <thorsten@zed.dev>
|
||||
|
||||
@@ -30,15 +30,6 @@
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"Proto": {
|
||||
"tab_size": 4,
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "clang-format",
|
||||
"arguments": ["-style={IndentWidth: 4, ColumnLimit: 0}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rust": {
|
||||
"tasks": {
|
||||
"variables": {
|
||||
|
||||
281
Cargo.lock
generated
281
Cargo.lock
generated
@@ -263,9 +263,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.93"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
||||
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -549,9 +549,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.17"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857"
|
||||
checksum = "7e614738943d3f68c628ae3dbce7c3daffb196665f82f8c8ea6b65de73c79429"
|
||||
dependencies = [
|
||||
"deflate64",
|
||||
"flate2",
|
||||
@@ -859,7 +859,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"http-types",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"hyper-rustls 0.24.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -915,6 +915,20 @@ dependencies = [
|
||||
"syn 2.0.76",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-tungstenite"
|
||||
version = "0.22.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tungstenite 0.19.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-tungstenite"
|
||||
version = "0.28.0"
|
||||
@@ -1099,9 +1113,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-runtime"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2424565416eef55906f9f8cece2072b6b6a76075e3ff81483ebe938a89a4c05f"
|
||||
checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
@@ -1123,6 +1137,28 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-kinesis"
|
||||
version = "1.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad48026d3d53881146469b36358d633f1b8c9ad6eb3033f348600f981f2f449b"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes 1.7.2",
|
||||
"http 0.2.12",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.47.0"
|
||||
@@ -1227,9 +1263,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "1.2.3"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be"
|
||||
checksum = "5619742a0d8f253be760bfbb8e8e8368c69e3587e4637af5754e488a611499b1"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-eventstream",
|
||||
@@ -1288,9 +1324,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-eventstream"
|
||||
version = "0.60.4"
|
||||
version = "0.60.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858"
|
||||
checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"bytes 1.7.2",
|
||||
@@ -1299,9 +1335,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.60.10"
|
||||
version = "0.60.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01dbcb6e2588fd64cfb6d7529661b06466419e4c54ed1c62d6510d2d0350a728"
|
||||
checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6"
|
||||
dependencies = [
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1339,9 +1375,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.7.1"
|
||||
version = "1.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87"
|
||||
checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
@@ -1354,7 +1390,7 @@ dependencies = [
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"hyper-rustls 0.24.2",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
@@ -1366,9 +1402,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api"
|
||||
version = "1.7.2"
|
||||
version = "1.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96"
|
||||
checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
@@ -1383,9 +1419,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "1.2.4"
|
||||
version = "1.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "273dcdfd762fae3e1650b8024624e7cd50e484e37abdab73a7a706188ad34543"
|
||||
checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"bytes 1.7.2",
|
||||
@@ -1445,7 +1481,7 @@ dependencies = [
|
||||
"headers",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
@@ -2304,9 +2340,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.21"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2314,9 +2350,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.21"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -2385,7 +2421,7 @@ dependencies = [
|
||||
"clickhouse-derive",
|
||||
"clickhouse-rs-cityhash-sys",
|
||||
"futures 0.3.30",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"hyper-tls",
|
||||
"lz4",
|
||||
"sealed",
|
||||
@@ -2424,7 +2460,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-native-tls",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tungstenite",
|
||||
"async-tungstenite 0.28.0",
|
||||
"chrono",
|
||||
"clock",
|
||||
"cocoa 0.26.0",
|
||||
@@ -2449,7 +2485,6 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2",
|
||||
"smol",
|
||||
"sysinfo",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"thiserror",
|
||||
@@ -2466,7 +2501,6 @@ dependencies = [
|
||||
name = "clock"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"smallvec",
|
||||
@@ -2557,9 +2591,10 @@ dependencies = [
|
||||
"assistant",
|
||||
"async-stripe",
|
||||
"async-trait",
|
||||
"async-tungstenite",
|
||||
"async-tungstenite 0.28.0",
|
||||
"audio",
|
||||
"aws-config",
|
||||
"aws-sdk-kinesis",
|
||||
"aws-sdk-s3",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
@@ -2588,7 +2623,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"hex",
|
||||
"http_client",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"indoc",
|
||||
"jsonwebtoken",
|
||||
"language",
|
||||
@@ -4189,6 +4224,7 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"task",
|
||||
"theme",
|
||||
"toml 0.8.19",
|
||||
"url",
|
||||
"util",
|
||||
@@ -4203,36 +4239,26 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"async-compression",
|
||||
"async-tar",
|
||||
"client",
|
||||
"collections",
|
||||
"context_servers",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.5",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"fs",
|
||||
"futures 0.3.30",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"indexed_docs",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"num-format",
|
||||
"parking_lot",
|
||||
"picker",
|
||||
"project",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"snippet_provider",
|
||||
@@ -4455,9 +4481,9 @@ checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -5274,12 +5300,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "handlebars"
|
||||
version = "6.2.0"
|
||||
version = "5.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd4ccde012831f9a071a637b0d4e31df31c0f6c525784b35ae76a9ac6bc1e315"
|
||||
checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"num-order",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"serde",
|
||||
@@ -5638,9 +5663,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.31"
|
||||
version = "0.14.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
|
||||
checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
|
||||
dependencies = [
|
||||
"bytes 1.7.2",
|
||||
"futures-channel",
|
||||
@@ -5653,7 +5678,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.10",
|
||||
"socket2 0.5.7",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -5688,7 +5713,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"log",
|
||||
"rustls 0.21.12",
|
||||
"rustls-native-certs 0.6.3",
|
||||
@@ -5721,7 +5746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes 1.7.2",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
@@ -6227,10 +6252,28 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-serde"
|
||||
version = "0.4.0"
|
||||
name = "jupyter-protocol"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd71aa17c4fa65e6d7536ab2728881a41f8feb2ee5841c2240516c3c3d65d8b3"
|
||||
checksum = "5f3e9d36f282f7e0400de20921d283121a97c5a5a6db2c1bb0c0853defff9934"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bytes 1.7.2",
|
||||
"chrono",
|
||||
"futures 0.3.30",
|
||||
"jupyter-serde",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-serde"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11adb69edaf2eb03d5e84249f68f870dd03d4c8f955314b5a32b2db5798e9b9a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6239,6 +6282,24 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-websocket-client"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d315d037789a652084877b0919615e937d2f2e877b01aa4ba8fcc1ab07cb58b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"async-tungstenite 0.22.2",
|
||||
"futures 0.3.30",
|
||||
"jupyter-protocol",
|
||||
"jupyter-serde",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
@@ -6313,7 +6374,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark 0.12.2",
|
||||
"pulldown-cmark 0.12.1",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rpc",
|
||||
@@ -6518,9 +6579,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.162"
|
||||
version = "0.2.159"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -6562,7 +6623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6626,18 +6687,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.31"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae"
|
||||
checksum = "3c943daedff228392b791b33bba32e75737756e80a613e32e246c6ce9cbab20a"
|
||||
dependencies = [
|
||||
"linkme-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkme-impl"
|
||||
version = "0.3.31"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9"
|
||||
checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6843,7 +6904,7 @@ dependencies = [
|
||||
"linkify",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"pulldown-cmark 0.12.2",
|
||||
"pulldown-cmark 0.12.1",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -6863,7 +6924,7 @@ dependencies = [
|
||||
"linkify",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark 0.12.2",
|
||||
"pulldown-cmark 0.12.1",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -6938,9 +6999,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mdbook"
|
||||
version = "0.4.42"
|
||||
version = "0.4.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7624879735513024d323e7267a0b3a7176aceb0db537939beb4ee31d9e8945e3"
|
||||
checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -6950,7 +7011,7 @@ dependencies = [
|
||||
"elasticlunr-rs",
|
||||
"env_logger 0.11.5",
|
||||
"futures-util",
|
||||
"handlebars 6.2.0",
|
||||
"handlebars 5.1.2",
|
||||
"ignore",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -7228,9 +7289,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nbformat"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9ffb2ca556072f114bcaf2ca01dde7f1bc8a4946097dd804cb5a22d8af7d6df"
|
||||
checksum = "187de1b1f1430353ef9b5208096d84f7bf089ee1593f14213d122b7fbb1f3dee"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -7504,21 +7565,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-modular"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
|
||||
|
||||
[[package]]
|
||||
name = "num-order"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
|
||||
dependencies = [
|
||||
"num-modular",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
@@ -8020,9 +8066,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.2"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
|
||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||
|
||||
[[package]]
|
||||
name = "pathfinder_geometry"
|
||||
@@ -8897,27 +8943,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.89"
|
||||
version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.16"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
|
||||
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
|
||||
dependencies = [
|
||||
"profiling-procmacros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling-procmacros"
|
||||
version = "1.0.16"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
|
||||
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
@@ -9164,9 +9210,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.12.2"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
|
||||
checksum = "666f0f59e259aea2d72e6012290c09877a780935cc3c18b1ceded41f3890d59c"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"memchr",
|
||||
@@ -9641,6 +9687,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9717,6 +9764,8 @@ dependencies = [
|
||||
"http_client",
|
||||
"image",
|
||||
"indoc",
|
||||
"jupyter-protocol",
|
||||
"jupyter-websocket-client",
|
||||
"language",
|
||||
"languages",
|
||||
"log",
|
||||
@@ -9759,7 +9808,7 @@ dependencies = [
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
@@ -9890,7 +9939,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"linkify",
|
||||
"pulldown-cmark 0.12.2",
|
||||
"pulldown-cmark 0.12.1",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -10001,7 +10050,7 @@ name = "rpc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite",
|
||||
"async-tungstenite 0.28.0",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"collections",
|
||||
@@ -10043,9 +10092,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "runtimelib"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe23ba9967355bbb1be2fb9a8e51bd239ffdf9c791fad5a9b765122ee2bde2e4"
|
||||
checksum = "2db079f82c110e25c3202d20c7cd29dcbfa93d96de7c5bb8bb6f294f477567cf"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-dispatcher",
|
||||
@@ -10057,8 +10106,8 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"futures 0.3.30",
|
||||
"glob",
|
||||
"jupyter-protocol",
|
||||
"jupyter-serde",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -13000,6 +13049,25 @@ version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes 1.7.2",
|
||||
"data-encoding",
|
||||
"http 0.2.12",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.20.1"
|
||||
@@ -13524,7 +13592,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.30",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
@@ -14237,7 +14305,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15319,7 +15387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_html"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -15331,13 +15399,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_ocaml"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.2.2"
|
||||
|
||||
87
Cargo.toml
87
Cargo.toml
@@ -148,7 +148,6 @@ members = [
|
||||
"extensions/haskell",
|
||||
"extensions/html",
|
||||
"extensions/lua",
|
||||
"extensions/ocaml",
|
||||
"extensions/php",
|
||||
"extensions/perplexity",
|
||||
"extensions/prisma",
|
||||
@@ -369,12 +368,14 @@ indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "2"
|
||||
itertools = "0.13.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { version = "0.2.0" }
|
||||
jupyter-websocket-client = { version = "0.4.1" }
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nbformat = "0.5.0"
|
||||
nbformat = "0.6.0"
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
@@ -389,7 +390,7 @@ pet-core = { git = "https://github.com/microsoft/python-environment-tools.git",
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
profiling = "1"
|
||||
prost = "0.9"
|
||||
prost-build = "0.9"
|
||||
@@ -408,7 +409,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
|
||||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.19.0", default-features = false, features = [
|
||||
runtimelib = { version = "0.21.0", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
@@ -563,45 +564,45 @@ ttf-parser = { opt-level = 3 }
|
||||
wasmtime-cranelift = { opt-level = 3 }
|
||||
wasmtime = { opt-level = 3 }
|
||||
# Build single-source-file crates with cg=1 as it helps make `cargo build` of a whole workspace a bit faster
|
||||
activity_indicator = {codegen-units = 1}
|
||||
assets = {codegen-units = 1}
|
||||
breadcrumbs = {codegen-units = 1}
|
||||
collections = {codegen-units = 1}
|
||||
command_palette = {codegen-units = 1}
|
||||
command_palette_hooks = {codegen-units = 1}
|
||||
evals = {codegen-units = 1}
|
||||
extension_cli = {codegen-units = 1}
|
||||
feature_flags = {codegen-units = 1}
|
||||
file_icons = {codegen-units = 1}
|
||||
fsevent = {codegen-units = 1}
|
||||
image_viewer = {codegen-units = 1}
|
||||
inline_completion_button = {codegen-units = 1}
|
||||
install_cli = {codegen-units = 1}
|
||||
journal = {codegen-units = 1}
|
||||
menu = {codegen-units = 1}
|
||||
notifications = {codegen-units = 1}
|
||||
ollama = {codegen-units = 1}
|
||||
outline = {codegen-units = 1}
|
||||
paths = {codegen-units = 1}
|
||||
prettier = {codegen-units = 1}
|
||||
project_symbols = {codegen-units = 1}
|
||||
refineable = {codegen-units = 1}
|
||||
release_channel = {codegen-units = 1}
|
||||
reqwest_client = {codegen-units = 1}
|
||||
rich_text = {codegen-units = 1}
|
||||
semantic_version = {codegen-units = 1}
|
||||
session = {codegen-units = 1}
|
||||
snippet = {codegen-units = 1}
|
||||
snippets_ui = {codegen-units = 1}
|
||||
sqlez_macros = {codegen-units = 1}
|
||||
story = {codegen-units = 1}
|
||||
supermaven_api = {codegen-units = 1}
|
||||
telemetry_events = {codegen-units = 1}
|
||||
theme_selector = {codegen-units = 1}
|
||||
time_format = {codegen-units = 1}
|
||||
ui_input = {codegen-units = 1}
|
||||
vcs_menu = {codegen-units = 1}
|
||||
zed_actions = {codegen-units = 1}
|
||||
activity_indicator = { codegen-units = 1 }
|
||||
assets = { codegen-units = 1 }
|
||||
breadcrumbs = { codegen-units = 1 }
|
||||
collections = { codegen-units = 1 }
|
||||
command_palette = { codegen-units = 1 }
|
||||
command_palette_hooks = { codegen-units = 1 }
|
||||
evals = { codegen-units = 1 }
|
||||
extension_cli = { codegen-units = 1 }
|
||||
feature_flags = { codegen-units = 1 }
|
||||
file_icons = { codegen-units = 1 }
|
||||
fsevent = { codegen-units = 1 }
|
||||
image_viewer = { codegen-units = 1 }
|
||||
inline_completion_button = { codegen-units = 1 }
|
||||
install_cli = { codegen-units = 1 }
|
||||
journal = { codegen-units = 1 }
|
||||
menu = { codegen-units = 1 }
|
||||
notifications = { codegen-units = 1 }
|
||||
ollama = { codegen-units = 1 }
|
||||
outline = { codegen-units = 1 }
|
||||
paths = { codegen-units = 1 }
|
||||
prettier = { codegen-units = 1 }
|
||||
project_symbols = { codegen-units = 1 }
|
||||
refineable = { codegen-units = 1 }
|
||||
release_channel = { codegen-units = 1 }
|
||||
reqwest_client = { codegen-units = 1 }
|
||||
rich_text = { codegen-units = 1 }
|
||||
semantic_version = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
sqlez_macros = { codegen-units = 1 }
|
||||
story = { codegen-units = 1 }
|
||||
supermaven_api = { codegen-units = 1 }
|
||||
telemetry_events = { codegen-units = 1 }
|
||||
theme_selector = { codegen-units = 1 }
|
||||
time_format = { codegen-units = 1 }
|
||||
ui_input = { codegen-units = 1 }
|
||||
vcs_menu = { codegen-units = 1 }
|
||||
zed_actions = { codegen-units = 1 }
|
||||
|
||||
[profile.release]
|
||||
debug = "limited"
|
||||
|
||||
@@ -251,6 +251,8 @@
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-shift-pageup": "pane::SwapItemLeft",
|
||||
"ctrl-shift-pagedown": "pane::SwapItemRight",
|
||||
"back": "pane::GoBack",
|
||||
"forward": "pane::GoForward",
|
||||
"ctrl-w": "pane::CloseActiveItem",
|
||||
"ctrl-f4": "pane::CloseActiveItem",
|
||||
"alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
|
||||
@@ -648,19 +650,23 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder",
|
||||
"context": "FileFinder && !menu_open",
|
||||
"bindings": {
|
||||
"ctrl-shift-p": "file_finder::SelectPrev",
|
||||
"ctrl-k": "file_finder::OpenMenu"
|
||||
"ctrl": "file_finder::OpenMenu",
|
||||
"ctrl-j": "pane::SplitDown",
|
||||
"ctrl-k": "pane::SplitUp",
|
||||
"ctrl-h": "pane::SplitLeft",
|
||||
"ctrl-l": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder && menu_open",
|
||||
"bindings": {
|
||||
"u": "pane::SplitUp",
|
||||
"d": "pane::SplitDown",
|
||||
"l": "pane::SplitLeft",
|
||||
"r": "pane::SplitRight"
|
||||
"j": "pane::SplitDown",
|
||||
"k": "pane::SplitUp",
|
||||
"h": "pane::SplitLeft",
|
||||
"l": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -648,19 +648,23 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder",
|
||||
"context": "FileFinder && !menu_open",
|
||||
"bindings": {
|
||||
"cmd-shift-p": "file_finder::SelectPrev",
|
||||
"cmd-k": "file_finder::OpenMenu"
|
||||
"cmd": "file_finder::OpenMenu",
|
||||
"cmd-j": "pane::SplitDown",
|
||||
"cmd-k": "pane::SplitUp",
|
||||
"cmd-h": "pane::SplitLeft",
|
||||
"cmd-l": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder && menu_open",
|
||||
"bindings": {
|
||||
"u": "pane::SplitUp",
|
||||
"d": "pane::SplitDown",
|
||||
"l": "pane::SplitLeft",
|
||||
"r": "pane::SplitRight"
|
||||
"j": "pane::SplitDown",
|
||||
"k": "pane::SplitUp",
|
||||
"h": "pane::SplitLeft",
|
||||
"l": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePrevItem"
|
||||
"ctrl-pagedown": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -18,6 +16,7 @@
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"alt-f3": "editor::SelectAllMatches", // find_all_under
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePrevItem"
|
||||
"ctrl-pagedown": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -21,6 +19,7 @@
|
||||
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
<task_description>
|
||||
|
||||
The user of a code editor wants to make a change to their codebase.
|
||||
You must describe the change using the following XML structure:
|
||||
|
||||
- <patch> - A group of related code changes.
|
||||
Child tags:
|
||||
- <title> (required) - A high-level description of the changes. This should be as short
|
||||
as possible, possibly using common abbreviations.
|
||||
- <edit> (1 or more) - An edit to make at a particular range within a file.
|
||||
Includes the following child tags:
|
||||
- <path> (required) - The path to the file that will be changed.
|
||||
- <description> (optional) - An arbitrarily-long comment that describes the purpose
|
||||
of this edit.
|
||||
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
|
||||
identifies a range within the file where the edit should occur. If this tag is not
|
||||
specified, then the entire file will be used as the range.
|
||||
- <new_text> (required) - The new text to insert into the file.
|
||||
- <operation> (required) - The type of change that should occur at the given range
|
||||
of the file. Must be one of the following values:
|
||||
- `update`: Replaces the entire range with the new text.
|
||||
- `insert_before`: Inserts the new text before the range.
|
||||
- `insert_after`: Inserts new text after the range.
|
||||
- `create`: Creates a new file with the given path and the new text.
|
||||
- `delete`: Deletes the specified range from the file.
|
||||
|
||||
<guidelines>
|
||||
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
|
||||
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
|
||||
- There's no need to escape angle brackets within XML tags.
|
||||
- Always ensure imports are added if you're referencing symbols that are not in scope.
|
||||
</guidelines>
|
||||
|
||||
Here are some concrete examples.
|
||||
|
||||
<example>
|
||||
<message role="user">
|
||||
|
||||
```rs src/shapes.rs
|
||||
pub mod rectangle;
|
||||
pub mod circle;
|
||||
```
|
||||
|
||||
```rs src/shapes/rectangle.rs
|
||||
pub struct Rectangle {
|
||||
width: f64,
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
pub fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```rs src/shapes/circle.rs
|
||||
pub struct Circle {
|
||||
radius: f64,
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
pub fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update all shapes to store their origin as an (x, y) tuple and implement Display.
|
||||
</message>
|
||||
|
||||
<message role="assistant">
|
||||
We'll need to update both the rectangle and circle modules.
|
||||
|
||||
<patch>
|
||||
<title>Add origins and display impls to shapes</title>
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>Add the origin field to Rectangle struct</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
pub struct Rectangle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
origin: (f64, f64),
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>update</operation>
|
||||
<old_text>
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
|
||||
Rectangle { origin, width, height }
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<description>Add the origin field to Circle struct</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
pub struct Circle {
|
||||
radius: f64,
|
||||
</old_text>
|
||||
<new_text>
|
||||
origin: (f64, f64),
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>update</operation>
|
||||
<old_text>
|
||||
fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
fn new(origin: (f64, f64), radius: f64) -> Self {
|
||||
Circle { origin, radius }
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<old_text>
|
||||
struct Rectangle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
use std::fmt;
|
||||
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>
|
||||
Add a manual Display implementation for Rectangle.
|
||||
Currently, this is the same as a derived Display implementation.
|
||||
</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
impl fmt::Display for Rectangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.format_struct(f, "Rectangle")
|
||||
.field("origin", &self.origin)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<old_text>
|
||||
struct Circle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
use std::fmt;
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
Circle { radius }
|
||||
}
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
impl fmt::Display for Rectangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.format_struct(f, "Rectangle")
|
||||
.field("origin", &self.origin)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
|
||||
</message>
|
||||
</example>
|
||||
|
||||
</task_description>
|
||||
@@ -157,7 +157,7 @@
|
||||
"auto_signature_help": false,
|
||||
/// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": true,
|
||||
"show_signature_help_after_edits": false,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
|
||||
@@ -490,6 +490,9 @@
|
||||
"version": "2",
|
||||
// Whether the assistant is enabled.
|
||||
"enabled": true,
|
||||
// Whether to show inline hints showing the keybindings to use the inline assistant and the
|
||||
// assistant panel.
|
||||
"show_hints": true,
|
||||
// Whether to show the assistant panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'.
|
||||
@@ -580,7 +583,23 @@
|
||||
// Settings related to the file finder.
|
||||
"file_finder": {
|
||||
// Whether to show file icons in the file finder.
|
||||
"file_icons": true
|
||||
"file_icons": true,
|
||||
// Determines how much space the file finder can take up in relation to the available window width.
|
||||
// There are 5 possible width values:
|
||||
//
|
||||
// 1. Small: This value is essentially a fixed width.
|
||||
// "modal_width": "small"
|
||||
// 2. Medium:
|
||||
// "modal_width": "medium"
|
||||
// 3. Large:
|
||||
// "modal_width": "large"
|
||||
// 4. Extra Large:
|
||||
// "modal_width": "xlarge"
|
||||
// 5. Fullscreen: This value removes any horizontal padding, as it consumes the whole viewport width.
|
||||
// "modal_width": "full"
|
||||
//
|
||||
// Default: small
|
||||
"modal_max_width": "small"
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
@@ -857,15 +876,8 @@
|
||||
//
|
||||
"file_types": {
|
||||
"Plain Text": ["txt"],
|
||||
"JSON": ["flake.lock"],
|
||||
"JSONC": [
|
||||
"**/.zed/**/*.json",
|
||||
"**/zed/**/*.json",
|
||||
"**/Zed/**/*.json",
|
||||
"tsconfig.json",
|
||||
"pyrightconfig.json"
|
||||
],
|
||||
"TOML": ["uv.lock"]
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json"],
|
||||
"Shell Script": [".env.*"]
|
||||
},
|
||||
/// By default use a recent system version of node, or install our own.
|
||||
/// You can override this to use a version of node that is not in $PATH with:
|
||||
@@ -1053,13 +1065,11 @@
|
||||
"api_url": "https://generativelanguage.googleapis.com"
|
||||
},
|
||||
"ollama": {
|
||||
"api_url": "http://localhost:11434",
|
||||
"low_speed_timeout_in_seconds": 60
|
||||
"api_url": "http://localhost:11434"
|
||||
},
|
||||
"openai": {
|
||||
"version": "1",
|
||||
"api_url": "https://api.openai.com/v1",
|
||||
"low_speed_timeout_in_seconds": 600
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
mod supported_countries;
|
||||
|
||||
use std::time::Duration;
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt as _;
|
||||
@@ -207,9 +205,8 @@ pub async fn stream_completion(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request, low_speed_timeout)
|
||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request)
|
||||
.await
|
||||
.map(|output| output.0)
|
||||
}
|
||||
@@ -261,7 +258,6 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<
|
||||
(
|
||||
BoxStream<'static, Result<Event, AnthropicError>>,
|
||||
@@ -274,7 +270,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
stream: true,
|
||||
};
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
@@ -284,9 +280,6 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
)
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("Content-Type", "application/json");
|
||||
if let Some(low_speed_timeout) = low_speed_timeout {
|
||||
request_builder = request_builder.read_timeout(low_speed_timeout);
|
||||
}
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).context("failed to serialize request")?;
|
||||
let request = request_builder
|
||||
|
||||
@@ -18,6 +18,7 @@ mod terminal_inline_assistant;
|
||||
mod tool_working_set;
|
||||
mod tools;
|
||||
|
||||
use crate::slash_command::project_command::ProjectSlashCommandFeatureFlag;
|
||||
pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet};
|
||||
pub use crate::tool_working_set::{ToolId, ToolWorkingSet};
|
||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||
@@ -215,23 +216,32 @@ pub fn init(
|
||||
});
|
||||
}
|
||||
|
||||
if cx.has_flag::<SearchSlashCommandFeatureFlag>() {
|
||||
cx.spawn(|mut cx| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
|
||||
let semantic_index = SemanticDb::new(
|
||||
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
cx.spawn(|mut cx| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
let is_search_slash_command_enabled = cx
|
||||
.update(|cx| cx.wait_for_flag::<SearchSlashCommandFeatureFlag>())?
|
||||
.await;
|
||||
let is_project_slash_command_enabled = cx
|
||||
.update(|cx| cx.wait_for_flag::<ProjectSlashCommandFeatureFlag>())?
|
||||
.await;
|
||||
|
||||
cx.update(|cx| cx.set_global(semantic_index))
|
||||
if !is_search_slash_command_enabled && !is_project_slash_command_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
|
||||
let semantic_index = SemanticDb::new(
|
||||
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
cx.update(|cx| cx.set_global(semantic_index))
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
context_store::init(&client.clone().into());
|
||||
prompt_library::init(cx);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::slash_command::file_command::codeblock_fence_for_path;
|
||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||
use crate::tools::code_edits_tool::{CodeEditsTool, CodeEditsToolInput};
|
||||
use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||
@@ -1898,47 +1899,111 @@ impl ContextEditor {
|
||||
let creases = new_tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
cx.view().downgrade(),
|
||||
IconName::PocketKnife,
|
||||
tool_use.name.clone().into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||
if &tool_use.name == CodeEditsTool::TOOL_NAME {
|
||||
// If this is a Code Edit tool,
|
||||
match serde_json::from_value::<CodeEditsToolInput>(
|
||||
tool_use.input.clone(),
|
||||
) {
|
||||
Ok(CodeEditsToolInput { title, edits }) => {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
cx.view().downgrade(),
|
||||
IconName::Sparkle,
|
||||
title.into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _cx: &mut WindowContext| {
|
||||
Empty.into_any()
|
||||
};
|
||||
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
|
||||
.unwrap();
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(
|
||||
excerpt_id,
|
||||
tool_use.source_range.start,
|
||||
)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(
|
||||
excerpt_id,
|
||||
tool_use.source_range.end,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
let buffer_row =
|
||||
MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.insert_content(
|
||||
Content::ToolUse {
|
||||
range: tool_use.source_range.clone(),
|
||||
tool_use: LanguageModelToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name.clone(),
|
||||
input: tool_use.input.clone(),
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.insert_content(
|
||||
Content::ToolUse {
|
||||
range: tool_use.source_range.clone(),
|
||||
tool_use: LanguageModelToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name.clone(),
|
||||
input: tool_use.input.clone(),
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
Crease::inline(
|
||||
start..end,
|
||||
placeholder,
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
)
|
||||
}
|
||||
Err(json_err) => {
|
||||
// TODO gracefully handle malformed JSON (should distinguish from "errored out" vs "not done streaming yet")
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
cx.view().downgrade(),
|
||||
IconName::PocketKnife,
|
||||
tool_use.name.clone().into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
|
||||
.unwrap();
|
||||
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.insert_content(
|
||||
Content::ToolUse {
|
||||
range: tool_use.source_range.clone(),
|
||||
tool_use: LanguageModelToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name.clone(),
|
||||
input: tool_use.input.clone(),
|
||||
},
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
Crease::inline(
|
||||
start..end,
|
||||
placeholder,
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
)
|
||||
Crease::inline(
|
||||
start..end,
|
||||
placeholder,
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -2050,30 +2115,6 @@ impl ContextEditor {
|
||||
ContextEvent::SlashCommandOutputSectionAdded { section } => {
|
||||
self.insert_slash_command_output_sections([section.clone()], false, cx);
|
||||
}
|
||||
ContextEvent::SlashCommandFinished {
|
||||
output_range: _output_range,
|
||||
run_commands_in_ranges,
|
||||
} => {
|
||||
for range in run_commands_in_ranges {
|
||||
let commands = self.context.update(cx, |context, cx| {
|
||||
context.reparse(cx);
|
||||
context
|
||||
.pending_commands_for_range(range.clone(), cx)
|
||||
.to_vec()
|
||||
});
|
||||
|
||||
for command in commands {
|
||||
self.run_command(
|
||||
command.source_range,
|
||||
&command.name,
|
||||
&command.arguments,
|
||||
false,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
.context
|
||||
@@ -2152,6 +2193,37 @@ impl ContextEditor {
|
||||
command_id: InvokedSlashCommandId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(invoked_slash_command) =
|
||||
self.context.read(cx).invoked_slash_command(&command_id)
|
||||
{
|
||||
if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
|
||||
let run_commands_in_ranges = invoked_slash_command
|
||||
.run_commands_in_ranges
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for range in run_commands_in_ranges {
|
||||
let commands = self.context.update(cx, |context, cx| {
|
||||
context.reparse(cx);
|
||||
context
|
||||
.pending_commands_for_range(range.clone(), cx)
|
||||
.to_vec()
|
||||
});
|
||||
|
||||
for command in commands {
|
||||
self.run_command(
|
||||
command.source_range,
|
||||
&command.name,
|
||||
&command.arguments,
|
||||
false,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if let Some(invoked_slash_command) =
|
||||
self.context.read(cx).invoked_slash_command(&command_id)
|
||||
@@ -3333,7 +3405,8 @@ impl ContextEditor {
|
||||
|
||||
self.context.update(cx, |context, cx| {
|
||||
for image in images {
|
||||
let Some(render_image) = image.to_image_data(cx).log_err() else {
|
||||
let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let image_id = image.id();
|
||||
@@ -3920,7 +3993,7 @@ impl ContextEditor {
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.max_h_32()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message.clone())),
|
||||
)
|
||||
|
||||
@@ -35,20 +35,17 @@ pub enum AssistantProviderContentV1 {
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
default_model: Option<AnthropicModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -63,6 +60,7 @@ pub struct AssistantSettings {
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
pub show_hints: bool,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
@@ -115,47 +113,41 @@ impl AssistantSettingsContent {
|
||||
if let VersionedAssistantSettingsContent::V1(settings) = settings {
|
||||
if let Some(provider) = settings.provider.clone() {
|
||||
match provider {
|
||||
AssistantProviderContentV1::Anthropic {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..
|
||||
} => update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.anthropic.is_none() {
|
||||
content.anthropic = Some(AnthropicSettingsContent::Versioned(
|
||||
VersionedAnthropicSettingsContent::V1(
|
||||
AnthropicSettingsContentV1 {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models: None,
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
AssistantProviderContentV1::Ollama {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..
|
||||
} => update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.ollama.is_none() {
|
||||
content.ollama = Some(OllamaSettingsContent {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models: None,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
AssistantProviderContentV1::Anthropic { api_url, .. } => {
|
||||
update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.anthropic.is_none() {
|
||||
content.anthropic =
|
||||
Some(AnthropicSettingsContent::Versioned(
|
||||
VersionedAnthropicSettingsContent::V1(
|
||||
AnthropicSettingsContentV1 {
|
||||
api_url,
|
||||
available_models: None,
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { api_url, .. } => {
|
||||
update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.ollama.is_none() {
|
||||
content.ollama = Some(OllamaSettingsContent {
|
||||
api_url,
|
||||
available_models: None,
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
..
|
||||
} => update_settings_file::<AllLanguageModelSettings>(
|
||||
@@ -188,7 +180,6 @@ impl AssistantSettingsContent {
|
||||
VersionedOpenAiSettingsContent::V1(
|
||||
OpenAiSettingsContentV1 {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
),
|
||||
@@ -212,6 +203,7 @@ impl AssistantSettingsContent {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
show_hints: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
@@ -252,6 +244,7 @@ impl AssistantSettingsContent {
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
@@ -298,54 +291,41 @@ impl AssistantSettingsContent {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let (api_url, low_speed_timeout_in_seconds) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..
|
||||
}) => (api_url.clone(), *low_speed_timeout_in_seconds),
|
||||
_ => (None, None),
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let (api_url, low_speed_timeout_in_seconds) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..
|
||||
}) => (api_url.clone(), *low_speed_timeout_in_seconds),
|
||||
_ => (None, None),
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, low_speed_timeout_in_seconds, available_models) =
|
||||
match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
..
|
||||
}) => (
|
||||
api_url.clone(),
|
||||
*low_speed_timeout_in_seconds,
|
||||
available_models.clone(),
|
||||
),
|
||||
_ => (None, None, None),
|
||||
};
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
@@ -377,6 +357,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
@@ -394,6 +375,11 @@ pub struct AssistantSettingsContentV2 {
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show inline hints that show keybindings for inline assistant
|
||||
/// and assistant panel.
|
||||
///
|
||||
/// Default: true
|
||||
show_hints: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
@@ -528,6 +514,7 @@ impl Settings for AssistantSettings {
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.show_hints, value.show_hints);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
@@ -598,6 +585,7 @@ mod tests {
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
|
||||
@@ -6,12 +6,14 @@ use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
prompts::PromptBuilder,
|
||||
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
|
||||
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
|
||||
tools::code_edits_tool::CodeEditsTool,
|
||||
AssistantPatch, MessageId, MessageStatus,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use assistant_tool::Tool;
|
||||
use client::{self, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -43,7 +45,6 @@ use std::{
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -381,10 +382,6 @@ pub enum ContextEvent {
|
||||
SlashCommandOutputSectionAdded {
|
||||
section: SlashCommandOutputSection<language::Anchor>,
|
||||
},
|
||||
SlashCommandFinished {
|
||||
output_range: Range<language::Anchor>,
|
||||
run_commands_in_ranges: Vec<Range<language::Anchor>>,
|
||||
},
|
||||
UsePendingTools,
|
||||
ToolFinished {
|
||||
tool_use_id: Arc<str>,
|
||||
@@ -565,7 +562,6 @@ pub struct Context {
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
patches: Vec<AssistantPatch>,
|
||||
xml_tags: Vec<XmlTag>,
|
||||
project: Option<Model<Project>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
@@ -674,7 +670,6 @@ impl Context {
|
||||
slash_commands,
|
||||
tools,
|
||||
patches: Vec::new(),
|
||||
xml_tags: Vec::new(),
|
||||
prompt_builder,
|
||||
};
|
||||
|
||||
@@ -916,6 +911,7 @@ impl Context {
|
||||
InvokedSlashCommand {
|
||||
name: name.into(),
|
||||
range: output_range,
|
||||
run_commands_in_ranges: Vec::new(),
|
||||
status: InvokedSlashCommandStatus::Running(Task::ready(())),
|
||||
transaction: None,
|
||||
timestamp: id.0,
|
||||
@@ -965,7 +961,6 @@ impl Context {
|
||||
}
|
||||
|
||||
if !changed_messages.is_empty() {
|
||||
self.message_roles_updated(changed_messages, cx);
|
||||
cx.emit(ContextEvent::MessagesEdited);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1389,8 +1384,6 @@ impl Context {
|
||||
|
||||
let mut removed_parsed_slash_command_ranges = Vec::new();
|
||||
let mut updated_parsed_slash_commands = Vec::new();
|
||||
let mut removed_patches = Vec::new();
|
||||
let mut updated_patches = Vec::new();
|
||||
while let Some(mut row_range) = row_ranges.next() {
|
||||
while let Some(next_row_range) = row_ranges.peek() {
|
||||
if row_range.end >= next_row_range.start {
|
||||
@@ -1415,13 +1408,6 @@ impl Context {
|
||||
cx,
|
||||
);
|
||||
self.invalidate_pending_slash_commands(&buffer, cx);
|
||||
self.reparse_patches_in_range(
|
||||
start..end,
|
||||
&buffer,
|
||||
&mut updated_patches,
|
||||
&mut removed_patches,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
if !updated_parsed_slash_commands.is_empty()
|
||||
@@ -1432,13 +1418,6 @@ impl Context {
|
||||
updated: updated_parsed_slash_commands,
|
||||
});
|
||||
}
|
||||
|
||||
if !updated_patches.is_empty() || !removed_patches.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated {
|
||||
removed: removed_patches,
|
||||
updated: updated_patches,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn reparse_slash_commands_in_range(
|
||||
@@ -1529,267 +1508,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
fn reparse_patches_in_range(
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
buffer: &BufferSnapshot,
|
||||
updated: &mut Vec<Range<text::Anchor>>,
|
||||
removed: &mut Vec<Range<text::Anchor>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// Rebuild the XML tags in the edited range.
|
||||
let intersecting_tags_range =
|
||||
self.indices_intersecting_buffer_range(&self.xml_tags, range.clone(), cx);
|
||||
let new_tags = self.parse_xml_tags_in_range(buffer, range.clone(), cx);
|
||||
self.xml_tags
|
||||
.splice(intersecting_tags_range.clone(), new_tags);
|
||||
|
||||
// Find which patches intersect the changed range.
|
||||
let intersecting_patches_range =
|
||||
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
|
||||
|
||||
// Reparse all tags after the last unchanged patch before the change.
|
||||
let mut tags_start_ix = 0;
|
||||
if let Some(preceding_unchanged_patch) =
|
||||
self.patches[..intersecting_patches_range.start].last()
|
||||
{
|
||||
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
||||
tag.range
|
||||
.start
|
||||
.cmp(&preceding_unchanged_patch.range.end, buffer)
|
||||
.then(Ordering::Less)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
}
|
||||
|
||||
// Rebuild the patches in the range.
|
||||
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
|
||||
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
|
||||
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
|
||||
removed.extend(
|
||||
removed_patches
|
||||
.map(|patch| patch.range)
|
||||
.filter(|range| !updated.contains(&range)),
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_xml_tags_in_range(
|
||||
&self,
|
||||
buffer: &BufferSnapshot,
|
||||
range: Range<text::Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<XmlTag> {
|
||||
let mut messages = self.messages(cx).peekable();
|
||||
|
||||
let mut tags = Vec::new();
|
||||
let mut lines = buffer.text_for_range(range).lines();
|
||||
let mut offset = lines.offset();
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
while let Some(message) = messages.peek() {
|
||||
if offset < message.offset_range.end {
|
||||
break;
|
||||
} else {
|
||||
messages.next();
|
||||
}
|
||||
}
|
||||
|
||||
let is_assistant_message = messages
|
||||
.peek()
|
||||
.map_or(false, |message| message.role == Role::Assistant);
|
||||
if is_assistant_message {
|
||||
for (start_ix, _) in line.match_indices('<') {
|
||||
let mut name_start_ix = start_ix + 1;
|
||||
let closing_bracket_ix = line[start_ix..].find('>').map(|i| start_ix + i);
|
||||
if let Some(closing_bracket_ix) = closing_bracket_ix {
|
||||
let end_ix = closing_bracket_ix + 1;
|
||||
let mut is_open_tag = true;
|
||||
if line[name_start_ix..closing_bracket_ix].starts_with('/') {
|
||||
name_start_ix += 1;
|
||||
is_open_tag = false;
|
||||
}
|
||||
let tag_inner = &line[name_start_ix..closing_bracket_ix];
|
||||
let tag_name_len = tag_inner
|
||||
.find(|c: char| c.is_whitespace())
|
||||
.unwrap_or(tag_inner.len());
|
||||
if let Ok(kind) = XmlTagKind::from_str(&tag_inner[..tag_name_len]) {
|
||||
tags.push(XmlTag {
|
||||
range: buffer.anchor_after(offset + start_ix)
|
||||
..buffer.anchor_before(offset + end_ix),
|
||||
is_open_tag,
|
||||
kind,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset = lines.offset();
|
||||
}
|
||||
tags
|
||||
}
|
||||
|
||||
fn parse_patches(
|
||||
&mut self,
|
||||
tags_start_ix: usize,
|
||||
buffer_end: text::Anchor,
|
||||
buffer: &BufferSnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Vec<AssistantPatch> {
|
||||
let mut new_patches = Vec::new();
|
||||
let mut pending_patch = None;
|
||||
let mut patch_tag_depth = 0;
|
||||
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
|
||||
'tags: while let Some(tag) = tags.next() {
|
||||
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
|
||||
patch_tag_depth += 1;
|
||||
let patch_start = tag.range.start;
|
||||
let mut edits = Vec::<Result<AssistantEdit>>::new();
|
||||
let mut patch = AssistantPatch {
|
||||
range: patch_start..patch_start,
|
||||
title: String::new().into(),
|
||||
edits: Default::default(),
|
||||
status: crate::AssistantPatchStatus::Pending,
|
||||
};
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
|
||||
patch_tag_depth -= 1;
|
||||
if patch_tag_depth == 0 {
|
||||
patch.range.end = tag.range.end;
|
||||
|
||||
// Include the line immediately after this <patch> tag if it's empty.
|
||||
let patch_end_offset = patch.range.end.to_offset(buffer);
|
||||
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
|
||||
if patch_end_chars.next() == Some('\n')
|
||||
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
|
||||
{
|
||||
let messages = self.messages_for_offsets(
|
||||
[patch_end_offset, patch_end_offset + 1],
|
||||
cx,
|
||||
);
|
||||
if messages.len() == 1 {
|
||||
patch.range.end = buffer.anchor_before(patch_end_offset + 1);
|
||||
}
|
||||
}
|
||||
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
if let (Ok(a), Ok(b)) = (a, b) {
|
||||
a.path.cmp(&b.path)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
patch.edits = edits.into();
|
||||
patch.status = AssistantPatchStatus::Ready;
|
||||
new_patches.push(patch);
|
||||
continue 'tags;
|
||||
}
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Title && tag.is_open_tag {
|
||||
let content_start = tag.range.end;
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
|
||||
let content_end = tag.range.start;
|
||||
patch.title =
|
||||
trimmed_text_in_range(buffer, content_start..content_end)
|
||||
.into();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
|
||||
let mut path = None;
|
||||
let mut old_text = None;
|
||||
let mut new_text = None;
|
||||
let mut operation = None;
|
||||
let mut description = None;
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
||||
edits.push(AssistantEdit::new(
|
||||
path,
|
||||
operation,
|
||||
old_text,
|
||||
new_text,
|
||||
description,
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
if tag.is_open_tag
|
||||
&& [
|
||||
XmlTagKind::Path,
|
||||
XmlTagKind::OldText,
|
||||
XmlTagKind::NewText,
|
||||
XmlTagKind::Operation,
|
||||
XmlTagKind::Description,
|
||||
]
|
||||
.contains(&tag.kind)
|
||||
{
|
||||
let kind = tag.kind;
|
||||
let content_start = tag.range.end;
|
||||
if let Some(tag) = tags.peek() {
|
||||
if tag.kind == kind && !tag.is_open_tag {
|
||||
let tag = tags.next().unwrap();
|
||||
let content_end = tag.range.start;
|
||||
let content = trimmed_text_in_range(
|
||||
buffer,
|
||||
content_start..content_end,
|
||||
);
|
||||
match kind {
|
||||
XmlTagKind::Path => path = Some(content),
|
||||
XmlTagKind::Operation => operation = Some(content),
|
||||
XmlTagKind::OldText => {
|
||||
old_text = Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
XmlTagKind::NewText => {
|
||||
new_text = Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
XmlTagKind::Description => {
|
||||
description =
|
||||
Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patch.edits = edits.into();
|
||||
pending_patch = Some(patch);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut pending_patch) = pending_patch {
|
||||
let patch_start = pending_patch.range.start.to_offset(buffer);
|
||||
if let Some(message) = self.message_for_offset(patch_start, cx) {
|
||||
if message.anchor_range.end == text::Anchor::MAX {
|
||||
pending_patch.range.end = text::Anchor::MAX;
|
||||
} else {
|
||||
let message_end = buffer.anchor_after(message.offset_range.end - 1);
|
||||
pending_patch.range.end = message_end;
|
||||
}
|
||||
} else {
|
||||
pending_patch.range.end = text::Anchor::MAX;
|
||||
}
|
||||
|
||||
new_patches.push(pending_patch);
|
||||
}
|
||||
|
||||
new_patches
|
||||
}
|
||||
|
||||
pub fn pending_command_for_position(
|
||||
&mut self,
|
||||
position: language::Anchor,
|
||||
@@ -1914,7 +1632,6 @@ impl Context {
|
||||
}
|
||||
|
||||
let mut pending_section_stack: Vec<PendingSection> = Vec::new();
|
||||
let mut run_commands_in_ranges: Vec<Range<language::Anchor>> = Vec::new();
|
||||
let mut last_role: Option<Role> = None;
|
||||
let mut last_section_range = None;
|
||||
|
||||
@@ -1980,7 +1697,13 @@ impl Context {
|
||||
|
||||
let end = this.buffer.read(cx).anchor_before(insert_position);
|
||||
if run_commands_in_text {
|
||||
run_commands_in_ranges.push(start..end);
|
||||
if let Some(invoked_slash_command) =
|
||||
this.invoked_slash_commands.get_mut(&command_id)
|
||||
{
|
||||
invoked_slash_command
|
||||
.run_commands_in_ranges
|
||||
.push(start..end);
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommandEvent::EndSection => {
|
||||
@@ -2100,6 +1823,7 @@ impl Context {
|
||||
InvokedSlashCommand {
|
||||
name: name.to_string().into(),
|
||||
range: command_range.clone(),
|
||||
run_commands_in_ranges: Vec::new(),
|
||||
status: InvokedSlashCommandStatus::Running(insert_output_task),
|
||||
transaction: Some(first_transaction),
|
||||
timestamp: command_id.0,
|
||||
@@ -2383,7 +2107,11 @@ impl Context {
|
||||
});
|
||||
Some(error.to_string())
|
||||
} else {
|
||||
let error_message = error.to_string().trim().to_string();
|
||||
let error_message = error
|
||||
.chain()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
cx.emit(ContextEvent::ShowAssistError(SharedString::from(
|
||||
error_message.clone(),
|
||||
)));
|
||||
@@ -2464,9 +2192,22 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
let tools = if let RequestType::SuggestEdits = request_type {
|
||||
vec![{
|
||||
let tool = CodeEditsTool;
|
||||
LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool.input_schema(),
|
||||
}
|
||||
}]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
tools,
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
};
|
||||
@@ -2539,25 +2280,6 @@ impl Context {
|
||||
completion_request.messages.push(request_message);
|
||||
}
|
||||
|
||||
if let RequestType::SuggestEdits = request_type {
|
||||
if let Ok(preamble) = self.prompt_builder.generate_suggest_edits_prompt() {
|
||||
let last_elem_index = completion_request.messages.len();
|
||||
|
||||
completion_request
|
||||
.messages
|
||||
.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(preamble)],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
// The preamble message should be sent right before the last actual user message.
|
||||
completion_request
|
||||
.messages
|
||||
.swap(last_elem_index, last_elem_index.saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
completion_request
|
||||
}
|
||||
|
||||
@@ -2581,28 +2303,6 @@ impl Context {
|
||||
self.update_metadata(*id, cx, |metadata| metadata.role = role);
|
||||
}
|
||||
}
|
||||
|
||||
self.message_roles_updated(ids, cx);
|
||||
}
|
||||
|
||||
fn message_roles_updated(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
|
||||
let mut ranges = Vec::new();
|
||||
for message in self.messages(cx) {
|
||||
if ids.contains(&message.id) {
|
||||
ranges.push(message.anchor_range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let buffer = self.buffer.read(cx).text_snapshot();
|
||||
let mut updated = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
for range in ranges {
|
||||
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||
}
|
||||
|
||||
if !updated.is_empty() || !removed.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_metadata(
|
||||
@@ -2887,7 +2587,7 @@ impl Context {
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
"Generate a concise 3-7 word title for this conversation, omitting punctuation"
|
||||
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`"
|
||||
.into(),
|
||||
],
|
||||
cache: false,
|
||||
@@ -3172,6 +2872,7 @@ pub struct ParsedSlashCommand {
|
||||
pub struct InvokedSlashCommand {
|
||||
pub name: SharedString,
|
||||
pub range: Range<language::Anchor>,
|
||||
pub run_commands_in_ranges: Vec<Range<language::Anchor>>,
|
||||
pub status: InvokedSlashCommandStatus,
|
||||
pub transaction: Option<language::TransactionId>,
|
||||
timestamp: clock::Lamport,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{AssistantEdit, MessageCacheMetadata};
|
||||
use super::MessageCacheMetadata;
|
||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||
use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
||||
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
|
||||
MessageStatus, PromptBuilder,
|
||||
};
|
||||
use crate::{AssistantEdit, ToolWorkingSet};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
|
||||
|
||||
@@ -310,10 +310,6 @@ impl PromptBuilder {
|
||||
.render("terminal_assistant_prompt", &context)
|
||||
}
|
||||
|
||||
pub fn generate_suggest_edits_prompt(&self) -> Result<String, RenderError> {
|
||||
self.handlebars.lock().render("suggest_edits", &())
|
||||
}
|
||||
|
||||
pub fn generate_project_slash_command_prompt(
|
||||
&self,
|
||||
context_buffer: String,
|
||||
|
||||
@@ -152,7 +152,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
if result
|
||||
.messages
|
||||
.iter()
|
||||
.any(|msg| !matches!(msg.role, context_servers::types::SamplingRole::User))
|
||||
.any(|msg| !matches!(msg.role, context_servers::types::Role::User))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Prompt contains non-user roles, which is not supported"
|
||||
@@ -164,7 +164,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
.messages
|
||||
.into_iter()
|
||||
.filter_map(|msg| match msg.content {
|
||||
context_servers::types::SamplingContent::Text { text } => Some(text),
|
||||
context_servers::types::MessageContent::Text { text } => Some(text),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
|
||||
@@ -69,6 +69,10 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod code_edits_tool;
|
||||
pub mod context_server_tool;
|
||||
pub mod now_tool;
|
||||
|
||||
86
crates/assistant/src/tools/code_edits_tool.rs
Normal file
86
crates/assistant/src/tools/code_edits_tool.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CodeEditsToolInput {
|
||||
/// A high-level description of the code changes. This should be as short as possible, possibly using common abbreviations.
|
||||
pub title: String,
|
||||
/// An array of edits to be applied.
|
||||
pub edits: Vec<Edit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Edit {
|
||||
/// The path to the file that this edit will change.
|
||||
pub path: String,
|
||||
/// An arbitrarily-long comment that describes the purpose of this edit.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// An excerpt from the file's current contents that uniquely identifies a range within the file where the edit should occur.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub old_text: Option<String>,
|
||||
/// The new text to insert into the file.
|
||||
pub new_text: String,
|
||||
/// The type of change that should occur at the given range of the file.
|
||||
pub operation: Operation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Operation {
|
||||
/// Replaces the entire range with the new text.
|
||||
Update,
|
||||
/// Inserts the new text before the range.
|
||||
InsertBefore,
|
||||
/// Inserts new text after the range.
|
||||
InsertAfter,
|
||||
/// Creates a new file with the given path and the new text.
|
||||
Create,
|
||||
/// Deletes the specified range from the file.
|
||||
Delete,
|
||||
}
|
||||
|
||||
pub struct CodeEditsTool;
|
||||
|
||||
impl CodeEditsTool {
|
||||
pub const TOOL_NAME: &str = "zed_code_edits";
|
||||
}
|
||||
|
||||
impl Tool for CodeEditsTool {
|
||||
fn name(&self) -> String {
|
||||
Self::TOOL_NAME.to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
// Anthropic's best practices for tool descriptions:
|
||||
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use#best-practices-for-tool-definitions
|
||||
include_str!("edit_tool_description.txt").to_string()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CodeEditsToolInput);
|
||||
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_workspace: WeakView<workspace::Workspace>,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<String>> {
|
||||
let input: CodeEditsToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let text = format!("The tool returned {:?}.", input);
|
||||
|
||||
Task::ready(Ok(text))
|
||||
}
|
||||
}
|
||||
@@ -74,11 +74,21 @@ impl Tool for ContextServerTool {
|
||||
);
|
||||
let response = protocol.run_tool(tool_name, arguments).await?;
|
||||
|
||||
let tool_result = match response.tool_result {
|
||||
serde_json::Value::String(s) => s,
|
||||
_ => serde_json::to_string(&response.tool_result)?,
|
||||
};
|
||||
Ok(tool_result)
|
||||
let mut result = String::new();
|
||||
for content in response.content {
|
||||
match content {
|
||||
types::ToolResponseContent::Text { text } => {
|
||||
result.push_str(&text);
|
||||
}
|
||||
types::ToolResponseContent::Image { .. } => {
|
||||
log::warn!("Ignoring image content from tool response");
|
||||
}
|
||||
types::ToolResponseContent::Resource { .. } => {
|
||||
log::warn!("Ignoring resource content from tool response");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
15
crates/assistant/src/tools/edit_tool_description.txt
Normal file
15
crates/assistant/src/tools/edit_tool_description.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
Describes the specific code changes that should be made to the files in a code base, based on the request the user made.
|
||||
It should be used when the user requests making changes to the code base, but not when the user is asking an question about information
|
||||
(including when asking for information about the code base) rather than requesting a change.
|
||||
|
||||
The tool will return an array of patches, each of which represents some related modifications to the code base.
|
||||
Each patch contains a high-level summary of the changes (which will be displayed in the code editor),
|
||||
as well as an array of specific edits to be made to specific individual files. The code editor will apply each of those edits to the code base, or not, at the discretion of the user of the editor.
|
||||
|
||||
Within each patch, the tool will never return multiple edits whose ranges intersect each other. Instead, it will merge them into one edit.
|
||||
On the other hand, for ranges that do not intersect each other, the tool will prefer multiple edits to smaller ranges over one edit to a larger range.
|
||||
|
||||
Whenever edits reference symbols that would be out of scope, the tool will always include earlier edits which add any necessary imports to bring those symbols into scope.
|
||||
|
||||
The overall goal is that if the user of the code editor accepts all edits within all patches, the code will end up in a correct state, and
|
||||
will successfully build and run without any further modifications from the user. It will also have correctly effected the changes to the code base that the user originally requested.
|
||||
@@ -343,7 +343,7 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
|
||||
@@ -42,7 +42,6 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -1780,7 +1780,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -1821,7 +1821,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -1900,7 +1900,7 @@ mod tests {
|
||||
let dropped_auth_count = Arc::new(Mutex::new(0));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -1943,7 +1943,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -2003,7 +2003,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -2038,7 +2038,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ mod event_coalescer;
|
||||
|
||||
use crate::{ChannelId, TelemetrySettings};
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::Future;
|
||||
@@ -15,12 +14,11 @@ use settings::{Settings, SettingsStore};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
|
||||
SettingEvent,
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
@@ -46,7 +44,7 @@ struct TelemetryState {
|
||||
flush_events_task: Option<Task<()>>,
|
||||
log_file: Option<File>,
|
||||
is_staff: Option<bool>,
|
||||
first_event_date_time: Option<DateTime<Utc>>,
|
||||
first_event_date_time: Option<Instant>,
|
||||
event_coalescer: EventCoalescer,
|
||||
max_queue_size: usize,
|
||||
worktree_id_map: WorktreeIdMap,
|
||||
@@ -293,55 +291,13 @@ impl Telemetry {
|
||||
state.session_id = Some(session_id);
|
||||
state.app_version = release_channel::AppVersion::global(cx).to_string();
|
||||
state.os_name = os_name();
|
||||
|
||||
drop(state);
|
||||
|
||||
let this = self.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let mut system = System::new_with_specifics(
|
||||
RefreshKind::new().with_cpu(CpuRefreshKind::everything()),
|
||||
);
|
||||
|
||||
let refresh_kind = ProcessRefreshKind::new().with_cpu().with_memory();
|
||||
let current_process = Pid::from_u32(std::process::id());
|
||||
system.refresh_processes_specifics(
|
||||
sysinfo::ProcessesToUpdate::Some(&[current_process]),
|
||||
refresh_kind,
|
||||
);
|
||||
|
||||
// Waiting some amount of time before the first query is important to get a reasonable value
|
||||
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
|
||||
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60);
|
||||
|
||||
loop {
|
||||
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
|
||||
|
||||
let current_process = Pid::from_u32(std::process::id());
|
||||
system.refresh_processes_specifics(
|
||||
sysinfo::ProcessesToUpdate::Some(&[current_process]),
|
||||
refresh_kind,
|
||||
);
|
||||
let Some(process) = system.process(current_process) else {
|
||||
log::error!(
|
||||
"Failed to find own process {current_process:?} in system process table"
|
||||
);
|
||||
// TODO: Fire an error telemetry event
|
||||
return;
|
||||
};
|
||||
|
||||
this.report_memory_event(process.memory(), process.virtual_memory());
|
||||
this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||
let state = self.state.lock();
|
||||
let enabled = state.settings.metrics;
|
||||
drop(state);
|
||||
return enabled;
|
||||
enabled
|
||||
}
|
||||
|
||||
pub fn set_authenticated_user_info(
|
||||
@@ -416,28 +372,6 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_cpu_event(self: &Arc<Self>, usage_as_percentage: f32, core_count: u32) {
|
||||
let event = Event::Cpu(CpuEvent {
|
||||
usage_as_percentage,
|
||||
core_count,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_memory_event(
|
||||
self: &Arc<Self>,
|
||||
memory_in_bytes: u64,
|
||||
virtual_memory_in_bytes: u64,
|
||||
) {
|
||||
let event = Event::Memory(MemoryEvent {
|
||||
memory_in_bytes,
|
||||
virtual_memory_in_bytes,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
|
||||
let event = Event::App(AppEvent { operation });
|
||||
|
||||
@@ -469,7 +403,10 @@ impl Telemetry {
|
||||
|
||||
if let Some((start, end, environment)) = period_data {
|
||||
let event = Event::Edit(EditEvent {
|
||||
duration: end.timestamp_millis() - start.timestamp_millis(),
|
||||
duration: end
|
||||
.saturating_duration_since(start)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
.as_millis() as i64,
|
||||
environment: environment.to_string(),
|
||||
is_via_ssh,
|
||||
});
|
||||
@@ -567,9 +504,10 @@ impl Telemetry {
|
||||
let date_time = self.clock.utc_now();
|
||||
|
||||
let milliseconds_since_first_event = match state.first_event_date_time {
|
||||
Some(first_event_date_time) => {
|
||||
date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
|
||||
}
|
||||
Some(first_event_date_time) => date_time
|
||||
.saturating_duration_since(first_event_date_time)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
.as_millis() as i64,
|
||||
None => {
|
||||
state.first_event_date_time = Some(date_time);
|
||||
0
|
||||
@@ -702,7 +640,6 @@ pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use http_client::FakeHttpClient;
|
||||
@@ -710,9 +647,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let system_id = Some("system_id".to_string());
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
@@ -743,7 +678,7 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
@@ -759,7 +694,7 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
@@ -775,7 +710,7 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
// Adding a 4th event should cause a flush
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
@@ -796,9 +731,7 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let system_id = Some("system_id".to_string());
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
use std::time;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use clock::SystemClock;
|
||||
|
||||
const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20);
|
||||
@@ -10,8 +9,8 @@ const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct PeriodData {
|
||||
environment: &'static str,
|
||||
start: DateTime<Utc>,
|
||||
end: Option<DateTime<Utc>>,
|
||||
start: Instant,
|
||||
end: Option<Instant>,
|
||||
}
|
||||
|
||||
pub struct EventCoalescer {
|
||||
@@ -27,9 +26,8 @@ impl EventCoalescer {
|
||||
pub fn log_event(
|
||||
&mut self,
|
||||
environment: &'static str,
|
||||
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
|
||||
) -> Option<(Instant, Instant, &'static str)> {
|
||||
let log_time = self.clock.utc_now();
|
||||
let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap();
|
||||
|
||||
let Some(state) = &mut self.state else {
|
||||
self.state = Some(PeriodData {
|
||||
@@ -43,7 +41,7 @@ impl EventCoalescer {
|
||||
let period_end = state
|
||||
.end
|
||||
.unwrap_or(state.start + SIMULATED_DURATION_FOR_SINGLE_EVENT);
|
||||
let within_timeout = log_time - period_end < coalesce_timeout;
|
||||
let within_timeout = log_time - period_end < COALESCE_TIMEOUT;
|
||||
let environment_is_same = state.environment == environment;
|
||||
let should_coaelesce = !within_timeout || !environment_is_same;
|
||||
|
||||
@@ -70,16 +68,13 @@ impl EventCoalescer {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::TimeZone;
|
||||
use clock::FakeSystemClock;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_same_context_exceeding_timeout() {
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let environment_1 = "environment_1";
|
||||
let mut event_coalescer = EventCoalescer::new(clock.clone());
|
||||
|
||||
@@ -98,7 +93,7 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
|
||||
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
|
||||
|
||||
// Ensure that many calls within the timeout don't start a new period
|
||||
for _ in 0..100 {
|
||||
@@ -118,7 +113,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let period_end = clock.utc_now();
|
||||
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
|
||||
let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2;
|
||||
// Logging an event exceeding the timeout should start a new period
|
||||
clock.advance(exceed_timeout_adjustment);
|
||||
let new_period_start = clock.utc_now();
|
||||
@@ -137,9 +132,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_different_environment_under_timeout() {
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let environment_1 = "environment_1";
|
||||
let mut event_coalescer = EventCoalescer::new(clock.clone());
|
||||
|
||||
@@ -158,7 +151,7 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
|
||||
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
|
||||
clock.advance(within_timeout_adjustment);
|
||||
let period_end = clock.utc_now();
|
||||
let period_data = event_coalescer.log_event(environment_1);
|
||||
@@ -193,9 +186,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_switching_environment_while_within_timeout() {
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let environment_1 = "environment_1";
|
||||
let mut event_coalescer = EventCoalescer::new(clock.clone());
|
||||
|
||||
@@ -214,7 +205,7 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
|
||||
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
|
||||
clock.advance(within_timeout_adjustment);
|
||||
let period_end = clock.utc_now();
|
||||
let environment_2 = "environment_2";
|
||||
@@ -240,9 +231,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_switching_environment_while_exceeding_timeout() {
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let environment_1 = "environment_1";
|
||||
let mut event_coalescer = EventCoalescer::new(clock.clone());
|
||||
|
||||
@@ -261,7 +250,7 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
|
||||
let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2;
|
||||
clock.advance(exceed_timeout_adjustment);
|
||||
let period_end = clock.utc_now();
|
||||
let environment_2 = "environment_2";
|
||||
|
||||
@@ -16,7 +16,6 @@ doctest = false
|
||||
test-support = ["dep:parking_lot"]
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
parking_lot = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::time::Instant;
|
||||
|
||||
pub trait SystemClock: Send + Sync {
|
||||
/// Returns the current date and time in UTC.
|
||||
fn utc_now(&self) -> DateTime<Utc>;
|
||||
fn utc_now(&self) -> Instant;
|
||||
}
|
||||
|
||||
pub struct RealSystemClock;
|
||||
|
||||
impl SystemClock for RealSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
Utc::now()
|
||||
fn utc_now(&self) -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeSystemClockState {
|
||||
now: DateTime<Utc>,
|
||||
now: Instant,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -24,36 +24,30 @@ pub struct FakeSystemClock {
|
||||
state: parking_lot::Mutex<FakeSystemClockState>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Default for FakeSystemClock {
|
||||
fn default() -> Self {
|
||||
Self::new(Utc::now())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeSystemClock {
|
||||
pub fn new(now: DateTime<Utc>) -> Self {
|
||||
let state = FakeSystemClockState { now };
|
||||
pub fn new() -> Self {
|
||||
let state = FakeSystemClockState {
|
||||
now: Instant::now(),
|
||||
};
|
||||
|
||||
Self {
|
||||
state: parking_lot::Mutex::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_now(&self, now: DateTime<Utc>) {
|
||||
pub fn set_now(&self, now: Instant) {
|
||||
self.state.lock().now = now;
|
||||
}
|
||||
|
||||
/// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration).
|
||||
pub fn advance(&self, duration: chrono::Duration) {
|
||||
pub fn advance(&self, duration: std::time::Duration) {
|
||||
self.state.lock().now += duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl SystemClock for FakeSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
fn utc_now(&self) -> Instant {
|
||||
self.state.lock().now
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ async-stripe.workspace = true
|
||||
async-tungstenite.workspace = true
|
||||
aws-config = { version = "1.1.5" }
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
aws-sdk-kinesis = "1.51.0"
|
||||
axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.4", features = ["erased-json"] }
|
||||
base64.workspace = true
|
||||
|
||||
@@ -174,6 +174,31 @@ spec:
|
||||
secretKeyRef:
|
||||
name: blob-store
|
||||
key: bucket
|
||||
- name: KINESIS_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: kinesis
|
||||
key: access_key
|
||||
- name: KINESIS_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: kinesis
|
||||
key: secret_key
|
||||
- name: KINESIS_STREAM
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: kinesis
|
||||
key: stream
|
||||
- name: KINESIS_REGION
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: kinesis
|
||||
key: region
|
||||
- name: BLOB_STORE_BUCKET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: blob-store
|
||||
key: bucket
|
||||
- name: CLICKHOUSE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -11,9 +11,11 @@ use axum::{
|
||||
routing::post,
|
||||
Extension, Router, TypedHeader,
|
||||
};
|
||||
use chrono::Duration;
|
||||
use rpc::ExtensionMetadata;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use telemetry_events::{
|
||||
@@ -21,6 +23,7 @@ use telemetry_events::{
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, Panic,
|
||||
ReplEvent, SettingEvent,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
const CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
|
||||
@@ -388,13 +391,6 @@ pub async fn post_events(
|
||||
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||
body: Bytes,
|
||||
) -> Result<()> {
|
||||
let Some(clickhouse_client) = app.clickhouse_client.clone() else {
|
||||
Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"not supported".into(),
|
||||
))?
|
||||
};
|
||||
|
||||
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
|
||||
return Err(Error::http(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -416,6 +412,34 @@ pub async fn post_events(
|
||||
};
|
||||
let country_code = country_code_header.map(|h| h.to_string());
|
||||
|
||||
let first_event_at = chrono::Utc::now()
|
||||
- chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
|
||||
|
||||
if let Some(kinesis_client) = app.kinesis_client.clone() {
|
||||
if let Some(stream) = app.config.kinesis_stream.clone() {
|
||||
let mut request = kinesis_client.put_records().stream_name(stream);
|
||||
for row in for_snowflake(request_body.clone(), first_event_at, country_code.clone()) {
|
||||
if let Some(data) = serde_json::to_vec(&row).log_err() {
|
||||
request = request.records(
|
||||
aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
|
||||
.partition_key(request_body.system_id.clone().unwrap_or_default())
|
||||
.data(data.into())
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
request.send().await.log_err();
|
||||
}
|
||||
};
|
||||
|
||||
let Some(clickhouse_client) = app.clickhouse_client.clone() else {
|
||||
Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"not supported".into(),
|
||||
))?
|
||||
};
|
||||
|
||||
let first_event_at = chrono::Utc::now()
|
||||
- chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
|
||||
|
||||
@@ -459,20 +483,7 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Cpu(_) | Event::Memory(_) => continue,
|
||||
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
@@ -923,6 +934,7 @@ pub struct CpuEventRow {
|
||||
}
|
||||
|
||||
impl CpuEventRow {
|
||||
#[allow(unused)]
|
||||
fn from_event(
|
||||
event: CpuEvent,
|
||||
wrapper: &EventWrapper,
|
||||
@@ -977,6 +989,7 @@ pub struct MemoryEventRow {
|
||||
}
|
||||
|
||||
impl MemoryEventRow {
|
||||
#[allow(unused)]
|
||||
fn from_event(
|
||||
event: MemoryEvent,
|
||||
wrapper: &EventWrapper,
|
||||
@@ -1364,3 +1377,259 @@ pub fn calculate_json_checksum(app: Arc<AppState>, json: &impl AsRef<[u8]>) -> O
|
||||
summer.update(checksum_seed);
|
||||
Some(summer.finalize().into_iter().collect())
|
||||
}
|
||||
|
||||
fn for_snowflake(
|
||||
body: EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
country_code: Option<String>,
|
||||
) -> impl Iterator<Item = SnowflakeRow> {
|
||||
body.events.into_iter().flat_map(move |event| {
|
||||
let timestamp =
|
||||
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
|
||||
let (event_type, mut event_properties) = match &event.event {
|
||||
Event::Editor(e) => (
|
||||
match e.operation.as_str() {
|
||||
"open" => "Editor Opened".to_string(),
|
||||
"save" => "Editor Saved".to_string(),
|
||||
_ => format!("Unknown Editor Event: {}", e.operation),
|
||||
},
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::InlineCompletion(e) => (
|
||||
format!(
|
||||
"Inline Completion {}",
|
||||
if e.suggestion_accepted {
|
||||
"Accepted"
|
||||
} else {
|
||||
"Discarded"
|
||||
}
|
||||
),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Call(e) => {
|
||||
let event_type = match e.operation.trim() {
|
||||
"unshare project" => "Project Unshared".to_string(),
|
||||
"open channel notes" => "Channel Notes Opened".to_string(),
|
||||
"share project" => "Project Shared".to_string(),
|
||||
"join channel" => "Channel Joined".to_string(),
|
||||
"hang up" => "Call Ended".to_string(),
|
||||
"accept incoming" => "Incoming Call Accepted".to_string(),
|
||||
"invite" => "Participant Invited".to_string(),
|
||||
"disable microphone" => "Microphone Disabled".to_string(),
|
||||
"enable microphone" => "Microphone Enabled".to_string(),
|
||||
"enable screen share" => "Screen Share Enabled".to_string(),
|
||||
"disable screen share" => "Screen Share Disabled".to_string(),
|
||||
"decline incoming" => "Incoming Call Declined".to_string(),
|
||||
"enable camera" => "Camera Enabled".to_string(),
|
||||
"disable camera" => "Camera Disabled".to_string(),
|
||||
_ => format!("Unknown Call Event: {}", e.operation),
|
||||
};
|
||||
|
||||
(event_type, serde_json::to_value(e).unwrap())
|
||||
}
|
||||
Event::Assistant(e) => (
|
||||
match e.phase {
|
||||
telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(),
|
||||
telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
|
||||
telemetry_events::AssistantPhase::Accepted => {
|
||||
"Assistant Response Accepted".to_string()
|
||||
}
|
||||
telemetry_events::AssistantPhase::Rejected => {
|
||||
"Assistant Response Rejected".to_string()
|
||||
}
|
||||
},
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Cpu(_) | Event::Memory(_) => return None,
|
||||
Event::App(e) => {
|
||||
let mut properties = json!({});
|
||||
let event_type = match e.operation.trim() {
|
||||
"extensions: install extension" => "Extension Installed".to_string(),
|
||||
"open" => "App Opened".to_string(),
|
||||
"project search: open" => "Project Search Opened".to_string(),
|
||||
"first open" => {
|
||||
properties["is_first_open"] = json!(true);
|
||||
"App First Opened".to_string()
|
||||
}
|
||||
"extensions: uninstall extension" => "Extension Uninstalled".to_string(),
|
||||
"welcome page: close" => "Welcome Page Closed".to_string(),
|
||||
"open project" => {
|
||||
properties["is_first_time"] = json!(false);
|
||||
"Project Opened".to_string()
|
||||
}
|
||||
"welcome page: install cli" => "CLI Installed".to_string(),
|
||||
"project diagnostics: open" => "Project Diagnostics Opened".to_string(),
|
||||
"extensions page: open" => "Extensions Page Opened".to_string(),
|
||||
"welcome page: change theme" => "Welcome Theme Changed".to_string(),
|
||||
"welcome page: toggle metric telemetry" => {
|
||||
properties["enabled"] = json!(false);
|
||||
"Welcome Telemetry Toggled".to_string()
|
||||
}
|
||||
"welcome page: change keymap" => "Keymap Changed".to_string(),
|
||||
"welcome page: toggle vim" => {
|
||||
properties["enabled"] = json!(false);
|
||||
"Welcome Vim Mode Toggled".to_string()
|
||||
}
|
||||
"welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
|
||||
"welcome page: toggle diagnostic telemetry" => {
|
||||
"Welcome Telemetry Toggled".to_string()
|
||||
}
|
||||
"welcome page: open" => "Welcome Page Opened".to_string(),
|
||||
"close" => "App Closed".to_string(),
|
||||
"markdown preview: open" => "Markdown Preview Opened".to_string(),
|
||||
"welcome page: open extensions" => "Extensions Page Opened".to_string(),
|
||||
"open node project" | "open pnpm project" | "open yarn project" => {
|
||||
properties["project_type"] = json!("node");
|
||||
properties["is_first_time"] = json!(false);
|
||||
"Project Opened".to_string()
|
||||
}
|
||||
"repl sessions: open" => "REPL Session Started".to_string(),
|
||||
"welcome page: toggle helix" => {
|
||||
properties["enabled"] = json!(false);
|
||||
"Helix Mode Toggled".to_string()
|
||||
}
|
||||
"welcome page: edit settings" => {
|
||||
properties["changed_settings"] = json!([]);
|
||||
"Settings Edited".to_string()
|
||||
}
|
||||
"welcome page: view docs" => "Documentation Viewed".to_string(),
|
||||
"open ssh project" => {
|
||||
properties["is_first_time"] = json!(false);
|
||||
"SSH Project Opened".to_string()
|
||||
}
|
||||
"create ssh server" => "SSH Server Created".to_string(),
|
||||
"create ssh project" => "SSH Project Created".to_string(),
|
||||
"first open for release channel" => {
|
||||
properties["is_first_for_channel"] = json!(true);
|
||||
"App First Opened For Release Channel".to_string()
|
||||
}
|
||||
"feature upsell: toggle vim" => {
|
||||
properties["source"] = json!("Feature Upsell");
|
||||
"Vim Mode Toggled".to_string()
|
||||
}
|
||||
_ => e
|
||||
.operation
|
||||
.strip_prefix("feature upsell: viewed docs (")
|
||||
.and_then(|s| s.strip_suffix(')'))
|
||||
.map_or_else(
|
||||
|| format!("Unknown App Event: {}", e.operation),
|
||||
|docs_url| {
|
||||
properties["url"] = json!(docs_url);
|
||||
properties["source"] = json!("Feature Upsell");
|
||||
"Documentation Viewed".to_string()
|
||||
},
|
||||
),
|
||||
};
|
||||
(event_type, properties)
|
||||
}
|
||||
Event::Setting(e) => (
|
||||
"Settings Changed".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Extension(e) => (
|
||||
"Extension Loaded".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Edit(e) => (
|
||||
"Editor Edited".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Action(e) => (
|
||||
"Action Invoked".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Repl(e) => (
|
||||
"Kernel Status Changed".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
};
|
||||
|
||||
if let serde_json::Value::Object(ref mut map) = event_properties {
|
||||
map.insert("app_version".to_string(), body.app_version.clone().into());
|
||||
map.insert("os_name".to_string(), body.os_name.clone().into());
|
||||
map.insert("os_version".to_string(), body.os_version.clone().into());
|
||||
map.insert("architecture".to_string(), body.architecture.clone().into());
|
||||
map.insert(
|
||||
"release_channel".to_string(),
|
||||
body.release_channel.clone().into(),
|
||||
);
|
||||
map.insert("signed_in".to_string(), event.signed_in.into());
|
||||
if let Some(country_code) = country_code.as_ref() {
|
||||
map.insert("country_code".to_string(), country_code.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
let user_properties = Some(serde_json::json!({
|
||||
"is_staff": body.is_staff,
|
||||
"Country": country_code.clone(),
|
||||
"OS": format!("{} {}", body.os_name, body.os_version.clone().unwrap_or_default()),
|
||||
"Version": body.app_version.clone(),
|
||||
}));
|
||||
|
||||
Some(SnowflakeRow {
|
||||
time: timestamp,
|
||||
user_id: body.metrics_id.clone(),
|
||||
device_id: body.system_id.clone(),
|
||||
event_type,
|
||||
event_properties,
|
||||
user_properties,
|
||||
insert_id: Some(Uuid::new_v4().to_string()),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SnowflakeRow {
|
||||
pub time: chrono::DateTime<chrono::Utc>,
|
||||
pub user_id: Option<String>,
|
||||
pub device_id: Option<String>,
|
||||
pub event_type: String,
|
||||
pub event_properties: serde_json::Value,
|
||||
pub user_properties: Option<serde_json::Value>,
|
||||
pub insert_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SnowflakeData {
|
||||
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
|
||||
pub installation_id: Option<String>,
|
||||
/// Identifier unique to each logged in Zed user (randomly generated on first sign in)
|
||||
/// Identifier unique to each Zed session (differs for each time you open Zed)
|
||||
pub session_id: Option<String>,
|
||||
pub metrics_id: Option<String>,
|
||||
/// True for Zed staff, otherwise false
|
||||
pub is_staff: Option<bool>,
|
||||
/// Zed version number
|
||||
pub app_version: String,
|
||||
pub os_name: String,
|
||||
pub os_version: Option<String>,
|
||||
pub architecture: String,
|
||||
/// Zed release channel (stable, preview, dev)
|
||||
pub release_channel: Option<String>,
|
||||
pub signed_in: bool,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub editor_event: Option<EditorEvent>,
|
||||
#[serde(flatten)]
|
||||
pub inline_completion_event: Option<InlineCompletionEvent>,
|
||||
#[serde(flatten)]
|
||||
pub call_event: Option<CallEvent>,
|
||||
#[serde(flatten)]
|
||||
pub assistant_event: Option<AssistantEvent>,
|
||||
#[serde(flatten)]
|
||||
pub cpu_event: Option<CpuEvent>,
|
||||
#[serde(flatten)]
|
||||
pub memory_event: Option<MemoryEvent>,
|
||||
#[serde(flatten)]
|
||||
pub app_event: Option<AppEvent>,
|
||||
#[serde(flatten)]
|
||||
pub setting_event: Option<SettingEvent>,
|
||||
#[serde(flatten)]
|
||||
pub extension_event: Option<ExtensionEvent>,
|
||||
#[serde(flatten)]
|
||||
pub edit_event: Option<EditEvent>,
|
||||
#[serde(flatten)]
|
||||
pub repl_event: Option<ReplEvent>,
|
||||
#[serde(flatten)]
|
||||
pub action_event: Option<ActionEvent>,
|
||||
}
|
||||
|
||||
@@ -170,6 +170,10 @@ pub struct Config {
|
||||
pub blob_store_access_key: Option<String>,
|
||||
pub blob_store_secret_key: Option<String>,
|
||||
pub blob_store_bucket: Option<String>,
|
||||
pub kinesis_region: Option<String>,
|
||||
pub kinesis_stream: Option<String>,
|
||||
pub kinesis_access_key: Option<String>,
|
||||
pub kinesis_secret_key: Option<String>,
|
||||
pub zed_environment: Arc<str>,
|
||||
pub openai_api_key: Option<Arc<str>>,
|
||||
pub google_ai_api_key: Option<Arc<str>>,
|
||||
@@ -238,6 +242,10 @@ impl Config {
|
||||
stripe_api_key: None,
|
||||
supermaven_admin_api_key: None,
|
||||
user_backfiller_github_access_token: None,
|
||||
kinesis_region: None,
|
||||
kinesis_access_key: None,
|
||||
kinesis_secret_key: None,
|
||||
kinesis_stream: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,6 +284,7 @@ pub struct AppState {
|
||||
pub rate_limiter: Arc<RateLimiter>,
|
||||
pub executor: Executor,
|
||||
pub clickhouse_client: Option<::clickhouse::Client>,
|
||||
pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
@@ -332,6 +341,11 @@ impl AppState {
|
||||
.clickhouse_url
|
||||
.as_ref()
|
||||
.and_then(|_| build_clickhouse_client(&config).log_err()),
|
||||
kinesis_client: if config.kinesis_access_key.is_some() {
|
||||
build_kinesis_client(&config).await.log_err()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
config,
|
||||
};
|
||||
Ok(Arc::new(this))
|
||||
@@ -381,6 +395,35 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::
|
||||
Ok(aws_sdk_s3::Client::new(&s3_config))
|
||||
}
|
||||
|
||||
async fn build_kinesis_client(config: &Config) -> anyhow::Result<aws_sdk_kinesis::Client> {
|
||||
let keys = aws_sdk_s3::config::Credentials::new(
|
||||
config
|
||||
.kinesis_access_key
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("missing kinesis_access_key"))?,
|
||||
config
|
||||
.kinesis_secret_key
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("missing kinesis_secret_key"))?,
|
||||
None,
|
||||
None,
|
||||
"env",
|
||||
);
|
||||
|
||||
let kinesis_config = aws_config::defaults(BehaviorVersion::latest())
|
||||
.region(Region::new(
|
||||
config
|
||||
.kinesis_region
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("missing blob_store_region"))?,
|
||||
))
|
||||
.credentials_provider(keys)
|
||||
.load()
|
||||
.await;
|
||||
|
||||
Ok(aws_sdk_kinesis::Client::new(&kinesis_config))
|
||||
}
|
||||
|
||||
fn build_clickhouse_client(config: &Config) -> anyhow::Result<::clickhouse::Client> {
|
||||
Ok(::clickhouse::Client::default()
|
||||
.with_url(
|
||||
|
||||
@@ -267,7 +267,6 @@ async fn perform_completion(
|
||||
anthropic::ANTHROPIC_API_URL,
|
||||
api_key,
|
||||
request,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
@@ -357,7 +356,6 @@ async fn perform_completion(
|
||||
open_ai::OPEN_AI_API_URL,
|
||||
api_key,
|
||||
serde_json::from_str(params.provider_request.get())?,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -390,7 +388,6 @@ async fn perform_completion(
|
||||
google_ai::API_URL,
|
||||
api_key,
|
||||
serde_json::from_str(params.provider_request.get())?,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -3621,7 +3621,6 @@ async fn count_language_model_tokens(
|
||||
google_ai::API_URL,
|
||||
api_key,
|
||||
serde_json::from_str(&request.request)?,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -4031,12 +4030,18 @@ async fn get_llm_api_token(
|
||||
Err(anyhow!("terms of service not accepted"))?
|
||||
}
|
||||
|
||||
let mut account_created_at = user.created_at;
|
||||
if let Some(github_created_at) = user.github_user_created_at {
|
||||
account_created_at = account_created_at.min(github_created_at);
|
||||
}
|
||||
if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE {
|
||||
Err(anyhow!("account too young"))?
|
||||
let has_llm_subscription = session.has_llm_subscription(&db).await?;
|
||||
|
||||
let bypass_account_age_check =
|
||||
has_llm_subscription || flags.iter().any(|flag| flag == "bypass-account-age-check");
|
||||
if !bypass_account_age_check {
|
||||
let mut account_created_at = user.created_at;
|
||||
if let Some(github_created_at) = user.github_user_created_at {
|
||||
account_created_at = account_created_at.min(github_created_at);
|
||||
}
|
||||
if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE {
|
||||
Err(anyhow!("account too young"))?
|
||||
}
|
||||
}
|
||||
|
||||
let billing_preferences = db.get_billing_preferences(user.id).await?;
|
||||
@@ -4046,7 +4051,7 @@ async fn get_llm_api_token(
|
||||
session.is_staff(),
|
||||
billing_preferences,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
session.has_llm_subscription(&db).await?,
|
||||
has_llm_subscription,
|
||||
session.current_plan(&db).await?,
|
||||
&session.app_state.config,
|
||||
)?;
|
||||
|
||||
@@ -1323,11 +1323,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
match (host_file, guest_file) {
|
||||
(Some(host_file), Some(guest_file)) => {
|
||||
assert_eq!(guest_file.path(), host_file.path());
|
||||
assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
|
||||
assert_eq!(
|
||||
guest_file.mtime(),
|
||||
host_file.mtime(),
|
||||
"guest {} mtime does not match host {} for path {:?} in project {}",
|
||||
assert_eq!(guest_file.disk_state(), host_file.disk_state(),
|
||||
"guest {} disk_state does not match host {} for path {:?} in project {}",
|
||||
guest_user_id,
|
||||
host_user_id,
|
||||
guest_file.path(),
|
||||
|
||||
@@ -168,7 +168,7 @@ impl TestServer {
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
||||
{
|
||||
@@ -512,6 +512,7 @@ impl TestServer {
|
||||
rate_limiter: Arc::new(RateLimiter::new(test_db.db().clone())),
|
||||
executor,
|
||||
clickhouse_client: None,
|
||||
kinesis_client: None,
|
||||
config: Config {
|
||||
http_port: 0,
|
||||
database_url: "".into(),
|
||||
@@ -550,6 +551,10 @@ impl TestServer {
|
||||
stripe_api_key: None,
|
||||
supermaven_admin_api_key: None,
|
||||
user_backfiller_github_access_token: None,
|
||||
kinesis_region: None,
|
||||
kinesis_stream: None,
|
||||
kinesis_access_key: None,
|
||||
kinesis_secret_key: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::{sync::Arc, time::Duration};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu,
|
||||
TabBar, Tooltip,
|
||||
Tab, TabBar, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
@@ -939,7 +939,7 @@ impl Render for ChatPanel {
|
||||
TabBar::new("chat_header").child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS))
|
||||
.h(Tab::container_height(cx))
|
||||
.px_2()
|
||||
.child(Label::new(
|
||||
self.active_chat
|
||||
|
||||
@@ -2521,7 +2521,7 @@ impl CollabPanel {
|
||||
.flex()
|
||||
.w_full()
|
||||
.when(!channel.is_root_channel(), |el| {
|
||||
el.on_drag(channel.clone(), move |channel, cx| {
|
||||
el.on_drag(channel.clone(), move |channel, _, cx| {
|
||||
cx.new_view(|_| DraggedChannelView {
|
||||
channel: channel.clone(),
|
||||
width,
|
||||
|
||||
@@ -19,7 +19,9 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tooltip};
|
||||
use ui::{
|
||||
h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{
|
||||
@@ -588,7 +590,7 @@ impl Render for NotificationPanel {
|
||||
.px_2()
|
||||
.py_1()
|
||||
// Match the height of the tab bar so they line up.
|
||||
.h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS))
|
||||
.h(Tab::container_height(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Label::new("Notifications"))
|
||||
|
||||
@@ -25,6 +25,13 @@ use util::TryFutureExt;
|
||||
const JSON_RPC_VERSION: &str = "2.0";
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
// Standard JSON-RPC error codes
|
||||
pub const PARSE_ERROR: i32 = -32700;
|
||||
pub const INVALID_REQUEST: i32 = -32600;
|
||||
pub const METHOD_NOT_FOUND: i32 = -32601;
|
||||
pub const INVALID_PARAMS: i32 = -32602;
|
||||
pub const INTERNAL_ERROR: i32 = -32603;
|
||||
|
||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
|
||||
type NotificationHandler = Box<dyn Send + FnMut(Value, AsyncAppContext)>;
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ use collections::HashMap;
|
||||
use crate::client::Client;
|
||||
use crate::types;
|
||||
|
||||
const PROTOCOL_VERSION: &str = "2024-10-07";
|
||||
|
||||
pub struct ModelContextProtocol {
|
||||
inner: Client,
|
||||
}
|
||||
@@ -23,10 +21,9 @@ impl ModelContextProtocol {
|
||||
}
|
||||
|
||||
fn supported_protocols() -> Vec<types::ProtocolVersion> {
|
||||
vec![
|
||||
types::ProtocolVersion::VersionString(PROTOCOL_VERSION.to_string()),
|
||||
types::ProtocolVersion::VersionNumber(1),
|
||||
]
|
||||
vec![types::ProtocolVersion(
|
||||
types::LATEST_PROTOCOL_VERSION.to_string(),
|
||||
)]
|
||||
}
|
||||
|
||||
pub async fn initialize(
|
||||
@@ -34,11 +31,13 @@ impl ModelContextProtocol {
|
||||
client_info: types::Implementation,
|
||||
) -> Result<InitializedContextServerProtocol> {
|
||||
let params = types::InitializeParams {
|
||||
protocol_version: types::ProtocolVersion::VersionString(PROTOCOL_VERSION.to_string()),
|
||||
protocol_version: types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
|
||||
capabilities: types::ClientCapabilities {
|
||||
experimental: None,
|
||||
sampling: None,
|
||||
roots: None,
|
||||
},
|
||||
meta: None,
|
||||
client_info,
|
||||
};
|
||||
|
||||
@@ -148,6 +147,7 @@ impl InitializedContextServerProtocol {
|
||||
let params = types::PromptsGetParams {
|
||||
name: prompt.as_ref().to_string(),
|
||||
arguments: Some(arguments),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let response: types::PromptsGetResponse = self
|
||||
@@ -170,6 +170,7 @@ impl InitializedContextServerProtocol {
|
||||
name: argument.into(),
|
||||
value: value.into(),
|
||||
},
|
||||
meta: None,
|
||||
};
|
||||
let result: types::CompletionCompleteResponse = self
|
||||
.inner
|
||||
@@ -210,6 +211,7 @@ impl InitializedContextServerProtocol {
|
||||
let params = types::CallToolParams {
|
||||
name: tool.as_ref().to_string(),
|
||||
arguments,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let response: types::CallToolResponse = self
|
||||
|
||||
@@ -2,8 +2,8 @@ use collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
|
||||
|
||||
pub enum RequestType {
|
||||
Initialize,
|
||||
CallTool,
|
||||
@@ -18,6 +18,7 @@ pub enum RequestType {
|
||||
Ping,
|
||||
ListTools,
|
||||
ListResourceTemplates,
|
||||
ListRoots,
|
||||
}
|
||||
|
||||
impl RequestType {
|
||||
@@ -36,16 +37,14 @@ impl RequestType {
|
||||
RequestType::Ping => "ping",
|
||||
RequestType::ListTools => "tools/list",
|
||||
RequestType::ListResourceTemplates => "resources/templates/list",
|
||||
RequestType::ListRoots => "roots/list",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ProtocolVersion {
|
||||
VersionString(String),
|
||||
VersionNumber(u32),
|
||||
}
|
||||
#[serde(transparent)]
|
||||
pub struct ProtocolVersion(pub String);
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -53,6 +52,8 @@ pub struct InitializeParams {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub capabilities: ClientCapabilities,
|
||||
pub client_info: Implementation,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -61,30 +62,40 @@ pub struct CallToolParams {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub arguments: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourcesUnsubscribeParams {
|
||||
pub uri: Url,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourcesSubscribeParams {
|
||||
pub uri: Url,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourcesReadParams {
|
||||
pub uri: Url,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoggingSetLevelParams {
|
||||
pub level: LoggingLevel,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -93,6 +104,8 @@ pub struct PromptsGetParams {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub arguments: Option<HashMap<String, String>>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -100,6 +113,8 @@ pub struct PromptsGetParams {
|
||||
pub struct CompletionCompleteParams {
|
||||
pub r#ref: CompletionReference,
|
||||
pub argument: CompletionArgument,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -145,12 +160,16 @@ pub struct InitializeResponse {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub capabilities: ServerCapabilities,
|
||||
pub server_info: Implementation,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourcesReadResponse {
|
||||
pub contents: Vec<ResourceContent>,
|
||||
pub contents: Vec<ResourceContents>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -159,29 +178,39 @@ pub struct ResourcesListResponse {
|
||||
pub resources: Vec<Resource>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_cursor: Option<String>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SamplingMessage {
|
||||
pub role: Role,
|
||||
pub content: MessageContent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SamplingMessage {
|
||||
pub role: SamplingRole,
|
||||
pub content: SamplingContent,
|
||||
pub struct PromptMessage {
|
||||
pub role: Role,
|
||||
pub content: MessageContent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SamplingRole {
|
||||
pub enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SamplingContent {
|
||||
pub enum MessageContent {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
#[serde(rename = "image")]
|
||||
Image { data: String, mime_type: String },
|
||||
#[serde(rename = "resource")]
|
||||
Resource { resource: ResourceContents },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -189,7 +218,9 @@ pub enum SamplingContent {
|
||||
pub struct PromptsGetResponse {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub messages: Vec<SamplingMessage>,
|
||||
pub messages: Vec<PromptMessage>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -198,12 +229,16 @@ pub struct PromptsListResponse {
|
||||
pub prompts: Vec<Prompt>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_cursor: Option<String>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompletionCompleteResponse {
|
||||
pub completion: CompletionResult,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -214,6 +249,8 @@ pub struct CompletionResult {
|
||||
pub total: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub has_more: Option<bool>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
@@ -243,6 +280,8 @@ pub struct ClientCapabilities {
|
||||
pub experimental: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sampling: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub roots: Option<RootsCapabilities>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -283,6 +322,13 @@ pub struct ToolsCapabilities {
|
||||
pub list_changed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RootsCapabilities {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub list_changed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tool {
|
||||
@@ -312,14 +358,28 @@ pub struct Resource {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourceContent {
|
||||
pub struct ResourceContents {
|
||||
pub uri: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TextResourceContents {
|
||||
pub uri: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
pub mime_type: Option<String>,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlobResourceContents {
|
||||
pub uri: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blob: Option<String>,
|
||||
pub mime_type: Option<String>,
|
||||
pub blob: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -338,8 +398,32 @@ pub struct ResourceTemplate {
|
||||
pub enum LoggingLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Notice,
|
||||
Warning,
|
||||
Error,
|
||||
Critical,
|
||||
Alert,
|
||||
Emergency,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ModelPreferences {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hints: Option<Vec<ModelHint>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cost_priority: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub speed_priority: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub intelligence_priority: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ModelHint {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -352,6 +436,7 @@ pub enum NotificationType {
|
||||
ResourcesListChanged,
|
||||
ToolsListChanged,
|
||||
PromptsListChanged,
|
||||
RootsListChanged,
|
||||
}
|
||||
|
||||
impl NotificationType {
|
||||
@@ -364,6 +449,7 @@ impl NotificationType {
|
||||
NotificationType::ResourcesListChanged => "notifications/resources/list_changed",
|
||||
NotificationType::ToolsListChanged => "notifications/tools/list_changed",
|
||||
NotificationType::PromptsListChanged => "notifications/prompts/list_changed",
|
||||
NotificationType::RootsListChanged => "notifications/roots/list_changed",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,6 +459,14 @@ impl NotificationType {
|
||||
pub enum ClientNotification {
|
||||
Initialized,
|
||||
Progress(ProgressParams),
|
||||
RootsListChanged,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ProgressToken {
|
||||
String(String),
|
||||
Number(f64),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -382,10 +476,10 @@ pub struct ProgressParams {
|
||||
pub progress: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub total: Option<f64>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
pub type ProgressToken = String;
|
||||
|
||||
pub enum CompletionTotal {
|
||||
Exact(u32),
|
||||
HasMore,
|
||||
@@ -410,7 +504,22 @@ pub struct Completion {
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CallToolResponse {
|
||||
pub tool_result: serde_json::Value,
|
||||
pub content: Vec<ToolResponseContent>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_error: Option<bool>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ToolResponseContent {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
#[serde(rename = "image")]
|
||||
Image { data: String, mime_type: String },
|
||||
#[serde(rename = "resource")]
|
||||
Resource { resource: ResourceContents },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -419,4 +528,22 @@ pub struct ListToolsResponse {
|
||||
pub tools: Vec<Tool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_cursor: Option<String>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListRootsResponse {
|
||||
pub roots: Vec<Root>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Root {
|
||||
pub uri: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1229,8 +1229,10 @@ mod tests {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn mtime(&self) -> Option<std::time::SystemTime> {
|
||||
unimplemented!()
|
||||
fn disk_state(&self) -> language::DiskState {
|
||||
language::DiskState::Present {
|
||||
mtime: std::time::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
@@ -1245,10 +1247,6 @@ mod tests {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::DateTime;
|
||||
use fs::Fs;
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Global};
|
||||
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use paths::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_file;
|
||||
@@ -254,7 +254,6 @@ impl CopilotChat {
|
||||
|
||||
pub async fn stream_completion(
|
||||
request: Request,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let Some(this) = cx.update(|cx| Self::global(cx)).ok().flatten() else {
|
||||
@@ -274,8 +273,7 @@ impl CopilotChat {
|
||||
let token = match api_token {
|
||||
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
|
||||
_ => {
|
||||
let token =
|
||||
request_api_token(&oauth_token, client.clone(), low_speed_timeout).await?;
|
||||
let token = request_api_token(&oauth_token, client.clone()).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.api_token = Some(token.clone());
|
||||
cx.notify();
|
||||
@@ -284,25 +282,17 @@ impl CopilotChat {
|
||||
}
|
||||
};
|
||||
|
||||
stream_completion(client.clone(), token.api_key, request, low_speed_timeout).await
|
||||
stream_completion(client.clone(), token.api_key, request).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_api_token(
|
||||
oauth_token: &str,
|
||||
client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<ApiToken> {
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Result<ApiToken> {
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::GET)
|
||||
.uri(COPILOT_CHAT_AUTH_URL)
|
||||
.header("Authorization", format!("token {}", oauth_token))
|
||||
.header("Accept", "application/json");
|
||||
|
||||
if let Some(low_speed_timeout) = low_speed_timeout {
|
||||
request_builder = request_builder.read_timeout(low_speed_timeout);
|
||||
}
|
||||
|
||||
let request = request_builder.body(AsyncBody::empty())?;
|
||||
|
||||
let mut response = client.send(request).await?;
|
||||
@@ -340,9 +330,8 @@ async fn stream_completion(
|
||||
client: Arc<dyn HttpClient>,
|
||||
api_key: String,
|
||||
request: Request,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(COPILOT_CHAT_COMPLETION_URL)
|
||||
.header(
|
||||
@@ -356,9 +345,6 @@ async fn stream_completion(
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat");
|
||||
|
||||
if let Some(low_speed_timeout) = low_speed_timeout {
|
||||
request_builder = request_builder.read_timeout(low_speed_timeout);
|
||||
}
|
||||
let is_streaming = request.stream;
|
||||
|
||||
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
|
||||
|
||||
@@ -727,6 +727,10 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
self.excerpts.read(cx).is_dirty(cx)
|
||||
}
|
||||
|
||||
fn has_deleted_file(&self, cx: &AppContext) -> bool {
|
||||
self.excerpts.read(cx).has_deleted_file(cx)
|
||||
}
|
||||
|
||||
fn has_conflict(&self, cx: &AppContext) -> bool {
|
||||
self.excerpts.read(cx).has_conflict(cx)
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ gpui::actions!(
|
||||
OpenExcerptsSplit,
|
||||
OpenProposedChangesEditor,
|
||||
OpenFile,
|
||||
OpenDocs,
|
||||
OpenPermalinkToLine,
|
||||
OpenUrl,
|
||||
Outdent,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{View, ViewContext, WindowContext};
|
||||
use language::Language;
|
||||
@@ -54,9 +52,9 @@ pub fn switch_source_header(
|
||||
cx.spawn(|_editor, mut cx| async move {
|
||||
let switch_source_header = switch_source_header_task
|
||||
.await
|
||||
.with_context(|| format!("Switch source/header LSP request for path \"{}\" failed", source_file))?;
|
||||
.with_context(|| format!("Switch source/header LSP request for path \"{source_file}\" failed"))?;
|
||||
if switch_source_header.0.is_empty() {
|
||||
log::info!("Clangd returned an empty string when requesting to switch source/header from \"{}\"", source_file);
|
||||
log::info!("Clangd returned an empty string when requesting to switch source/header from \"{source_file}\"" );
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -67,14 +65,17 @@ pub fn switch_source_header(
|
||||
)
|
||||
})?;
|
||||
|
||||
let path = goto.to_file_path().map_err(|()| {
|
||||
anyhow::anyhow!("URL conversion to file path failed for \"{goto}\"")
|
||||
})?;
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, view_cx| {
|
||||
workspace.open_abs_path(PathBuf::from(goto.path()), false, view_cx)
|
||||
workspace.open_abs_path(path, false, view_cx)
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Switch source/header could not open \"{}\" in workspace",
|
||||
goto.path()
|
||||
"Switch source/header could not open \"{goto}\" in workspace"
|
||||
)
|
||||
})?
|
||||
.await
|
||||
|
||||
@@ -5,6 +5,7 @@ use gpui::{Task, ViewContext};
|
||||
|
||||
use crate::Editor;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DebouncedDelay {
|
||||
task: Option<Task<()>>,
|
||||
cancel_channel: Option<oneshot::Sender<()>>,
|
||||
|
||||
@@ -66,7 +66,7 @@ use std::{
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::{TabMap, TabSnapshot};
|
||||
use text::LineIndent;
|
||||
use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
|
||||
use ui::{px, SharedString, WindowContext};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use wrap_map::{WrapMap, WrapSnapshot};
|
||||
|
||||
@@ -541,11 +541,17 @@ pub struct HighlightStyles {
|
||||
pub suggestion: Option<HighlightStyle>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ChunkReplacement {
|
||||
Renderer(ChunkRenderer),
|
||||
Str(SharedString),
|
||||
}
|
||||
|
||||
pub struct HighlightedChunk<'a> {
|
||||
pub text: &'a str,
|
||||
pub style: Option<HighlightStyle>,
|
||||
pub is_tab: bool,
|
||||
pub renderer: Option<ChunkRenderer>,
|
||||
pub replacement: Option<ChunkReplacement>,
|
||||
}
|
||||
|
||||
impl<'a> HighlightedChunk<'a> {
|
||||
@@ -557,7 +563,7 @@ impl<'a> HighlightedChunk<'a> {
|
||||
let mut text = self.text;
|
||||
let style = self.style;
|
||||
let is_tab = self.is_tab;
|
||||
let renderer = self.renderer;
|
||||
let renderer = self.replacement;
|
||||
iter::from_fn(move || {
|
||||
let mut prefix_len = 0;
|
||||
while let Some(&ch) = chars.peek() {
|
||||
@@ -573,30 +579,33 @@ impl<'a> HighlightedChunk<'a> {
|
||||
text: prefix,
|
||||
style,
|
||||
is_tab,
|
||||
renderer: renderer.clone(),
|
||||
replacement: renderer.clone(),
|
||||
});
|
||||
}
|
||||
chars.next();
|
||||
let (prefix, suffix) = text.split_at(ch.len_utf8());
|
||||
text = suffix;
|
||||
if let Some(replacement) = replacement(ch) {
|
||||
let background = editor_style.status.hint_background;
|
||||
let underline = editor_style.status.hint;
|
||||
let invisible_highlight = HighlightStyle {
|
||||
background_color: Some(editor_style.status.hint_background),
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(editor_style.status.hint),
|
||||
thickness: px(1.),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let invisible_style = if let Some(mut style) = style {
|
||||
style.highlight(invisible_highlight);
|
||||
style
|
||||
} else {
|
||||
invisible_highlight
|
||||
};
|
||||
return Some(HighlightedChunk {
|
||||
text: prefix,
|
||||
style: None,
|
||||
style: Some(invisible_style),
|
||||
is_tab: false,
|
||||
renderer: Some(ChunkRenderer {
|
||||
render: Arc::new(move |_| {
|
||||
div()
|
||||
.child(replacement)
|
||||
.bg(background)
|
||||
.text_decoration_1()
|
||||
.text_decoration_color(underline)
|
||||
.into_any_element()
|
||||
}),
|
||||
constrain_width: false,
|
||||
}),
|
||||
replacement: Some(ChunkReplacement::Str(replacement.into())),
|
||||
});
|
||||
} else {
|
||||
let invisible_highlight = HighlightStyle {
|
||||
@@ -619,7 +628,7 @@ impl<'a> HighlightedChunk<'a> {
|
||||
text: prefix,
|
||||
style: Some(invisible_style),
|
||||
is_tab: false,
|
||||
renderer: renderer.clone(),
|
||||
replacement: renderer.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -631,7 +640,7 @@ impl<'a> HighlightedChunk<'a> {
|
||||
text: remainder,
|
||||
style,
|
||||
is_tab,
|
||||
renderer: renderer.clone(),
|
||||
replacement: renderer.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -895,7 +904,7 @@ impl DisplaySnapshot {
|
||||
text: chunk.text,
|
||||
style: highlight_style,
|
||||
is_tab: chunk.is_tab,
|
||||
renderer: chunk.renderer,
|
||||
replacement: chunk.renderer.map(ChunkReplacement::Renderer),
|
||||
}
|
||||
.highlight_invisibles(editor_style)
|
||||
})
|
||||
|
||||
@@ -540,6 +540,15 @@ pub enum IsVimMode {
|
||||
No,
|
||||
}
|
||||
|
||||
pub trait ActiveLineTrailerProvider {
|
||||
fn render_active_line_trailer(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
focus_handle: &FocusHandle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement>;
|
||||
}
|
||||
|
||||
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
|
||||
///
|
||||
/// See the [module level documentation](self) for more information.
|
||||
@@ -667,6 +676,7 @@ pub struct Editor {
|
||||
next_scroll_position: NextScrollCursorCenterTopBottom,
|
||||
addons: HashMap<TypeId, Box<dyn Addon>>,
|
||||
_scroll_cursor_center_top_bottom_task: Task<()>,
|
||||
active_line_trailer_provider: Option<Box<dyn ActiveLineTrailerProvider>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
@@ -883,6 +893,7 @@ struct AutocloseRegion {
|
||||
struct SnippetState {
|
||||
ranges: Vec<Vec<Range<Anchor>>>,
|
||||
active_index: usize,
|
||||
choices: Vec<Option<Vec<String>>>,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -1000,7 +1011,7 @@ enum ContextMenuOrigin {
|
||||
GutterIndicator(DisplayRow),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct CompletionsMenu {
|
||||
id: CompletionId,
|
||||
sort_completions: bool,
|
||||
@@ -1011,10 +1022,105 @@ struct CompletionsMenu {
|
||||
matches: Arc<[StringMatch]>,
|
||||
selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_completion_documentation_resolve_debounce: Arc<Mutex<DebouncedDelay>>,
|
||||
selected_completion_documentation_resolve_debounce: Option<Arc<Mutex<DebouncedDelay>>>,
|
||||
}
|
||||
|
||||
impl CompletionsMenu {
|
||||
fn new(
|
||||
id: CompletionId,
|
||||
sort_completions: bool,
|
||||
initial_position: Anchor,
|
||||
buffer: Model<Buffer>,
|
||||
completions: Box<[Completion]>,
|
||||
) -> Self {
|
||||
let match_candidates = completions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
completion.label.text[completion.label.filter_range.clone()].into(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position,
|
||||
buffer,
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
|
||||
DebouncedDelay::new(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_snippet_choices(
|
||||
id: CompletionId,
|
||||
sort_completions: bool,
|
||||
choices: &Vec<String>,
|
||||
selection: Range<Anchor>,
|
||||
buffer: Model<Buffer>,
|
||||
) -> Self {
|
||||
let completions = choices
|
||||
.iter()
|
||||
.map(|choice| Completion {
|
||||
old_range: selection.start.text_anchor..selection.end.text_anchor,
|
||||
new_text: choice.to_string(),
|
||||
label: CodeLabel {
|
||||
text: choice.to_string(),
|
||||
runs: Default::default(),
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let match_candidates = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
|
||||
.collect();
|
||||
let matches = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatch {
|
||||
candidate_id: id,
|
||||
score: 1.,
|
||||
positions: vec![],
|
||||
string: completion.clone(),
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position: selection.start,
|
||||
buffer,
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches,
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
|
||||
DebouncedDelay::new(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn suppress_documentation_resolution(mut self) -> Self {
|
||||
self.selected_completion_documentation_resolve_debounce
|
||||
.take();
|
||||
self
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
@@ -1115,6 +1221,12 @@ impl CompletionsMenu {
|
||||
let Some(provider) = provider else {
|
||||
return;
|
||||
};
|
||||
let Some(documentation_resolve) = self
|
||||
.selected_completion_documentation_resolve_debounce
|
||||
.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
@@ -1127,15 +1239,13 @@ impl CompletionsMenu {
|
||||
EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
|
||||
let delay = Duration::from_millis(delay_ms);
|
||||
|
||||
self.selected_completion_documentation_resolve_debounce
|
||||
.lock()
|
||||
.fire_new(delay, cx, |_, cx| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
documentation_resolve.lock().fire_new(delay, cx, |_, cx| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
@@ -1418,6 +1528,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AvailableCodeAction {
|
||||
excerpt_id: ExcerptId,
|
||||
action: CodeAction,
|
||||
@@ -2104,6 +2215,7 @@ impl Editor {
|
||||
addons: HashMap::default(),
|
||||
_scroll_cursor_center_top_bottom_task: Task::ready(()),
|
||||
text_style_refinement: None,
|
||||
active_line_trailer_provider: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
@@ -2392,6 +2504,16 @@ impl Editor {
|
||||
self.refresh_inline_completion(false, false, cx);
|
||||
}
|
||||
|
||||
pub fn set_active_line_trailer_provider<T>(
|
||||
&mut self,
|
||||
provider: Option<T>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
T: ActiveLineTrailerProvider + 'static,
|
||||
{
|
||||
self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>);
|
||||
}
|
||||
|
||||
pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> {
|
||||
self.placeholder_text.as_deref()
|
||||
}
|
||||
@@ -4386,6 +4508,10 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let position = self.selections.newest_anchor().head();
|
||||
let (buffer, buffer_position) =
|
||||
if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) {
|
||||
@@ -4431,30 +4557,13 @@ impl Editor {
|
||||
})?;
|
||||
let completions = completions.await.log_err();
|
||||
let menu = if let Some(completions) = completions {
|
||||
let mut menu = CompletionsMenu {
|
||||
let mut menu = CompletionsMenu::new(
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position: position,
|
||||
match_candidates: completions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
completion.label.text[completion.label.filter_range.clone()]
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
buffer: buffer.clone(),
|
||||
completions: Arc::new(RwLock::new(completions.into())),
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
selected_completion_documentation_resolve_debounce: Arc::new(Mutex::new(
|
||||
DebouncedDelay::new(),
|
||||
)),
|
||||
};
|
||||
position,
|
||||
buffer.clone(),
|
||||
completions.into(),
|
||||
);
|
||||
menu.filter(query.as_deref(), cx.background_executor().clone())
|
||||
.await;
|
||||
|
||||
@@ -4657,7 +4766,11 @@ impl Editor {
|
||||
self.transact(cx, |this, cx| {
|
||||
if let Some(mut snippet) = snippet {
|
||||
snippet.text = text.to_string();
|
||||
for tabstop in snippet.tabstops.iter_mut().flatten() {
|
||||
for tabstop in snippet
|
||||
.tabstops
|
||||
.iter_mut()
|
||||
.flat_map(|tabstop| tabstop.ranges.iter_mut())
|
||||
{
|
||||
tabstop.start -= common_prefix_len as isize;
|
||||
tabstop.end -= common_prefix_len as isize;
|
||||
}
|
||||
@@ -5693,6 +5806,27 @@ impl Editor {
|
||||
context_menu
|
||||
}
|
||||
|
||||
fn show_snippet_choices(
|
||||
&mut self,
|
||||
choices: &Vec<String>,
|
||||
selection: Range<Anchor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if selection.start.buffer_id.is_none() {
|
||||
return;
|
||||
}
|
||||
let buffer_id = selection.start.buffer_id.unwrap();
|
||||
let buffer = self.buffer().read(cx).buffer(buffer_id);
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
|
||||
if let Some(buffer) = buffer {
|
||||
*self.context_menu.write() = Some(ContextMenu::Completions(
|
||||
CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer)
|
||||
.suppress_documentation_resolution(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_snippet(
|
||||
&mut self,
|
||||
insertion_ranges: &[Range<usize>],
|
||||
@@ -5702,6 +5836,7 @@ impl Editor {
|
||||
struct Tabstop<T> {
|
||||
is_end_tabstop: bool,
|
||||
ranges: Vec<Range<T>>,
|
||||
choices: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
let tabstops = self.buffer.update(cx, |buffer, cx| {
|
||||
@@ -5721,10 +5856,11 @@ impl Editor {
|
||||
.tabstops
|
||||
.iter()
|
||||
.map(|tabstop| {
|
||||
let is_end_tabstop = tabstop.first().map_or(false, |tabstop| {
|
||||
let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| {
|
||||
tabstop.is_empty() && tabstop.start == snippet.text.len() as isize
|
||||
});
|
||||
let mut tabstop_ranges = tabstop
|
||||
.ranges
|
||||
.iter()
|
||||
.flat_map(|tabstop_range| {
|
||||
let mut delta = 0_isize;
|
||||
@@ -5746,6 +5882,7 @@ impl Editor {
|
||||
Tabstop {
|
||||
is_end_tabstop,
|
||||
ranges: tabstop_ranges,
|
||||
choices: tabstop.choices.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -5755,16 +5892,29 @@ impl Editor {
|
||||
s.select_ranges(tabstop.ranges.iter().cloned());
|
||||
});
|
||||
|
||||
if let Some(choices) = &tabstop.choices {
|
||||
if let Some(selection) = tabstop.ranges.first() {
|
||||
self.show_snippet_choices(choices, selection.clone(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're already at the last tabstop and it's at the end of the snippet,
|
||||
// we're done, we don't need to keep the state around.
|
||||
if !tabstop.is_end_tabstop {
|
||||
let choices = tabstops
|
||||
.iter()
|
||||
.map(|tabstop| tabstop.choices.clone())
|
||||
.collect();
|
||||
|
||||
let ranges = tabstops
|
||||
.into_iter()
|
||||
.map(|tabstop| tabstop.ranges)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.snippet_stack.push(SnippetState {
|
||||
active_index: 0,
|
||||
ranges,
|
||||
choices,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5839,6 +5989,13 @@ impl Editor {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_anchor_ranges(current_ranges.iter().cloned())
|
||||
});
|
||||
|
||||
if let Some(choices) = &snippet.choices[snippet.active_index] {
|
||||
if let Some(selection) = current_ranges.first() {
|
||||
self.show_snippet_choices(&choices, selection.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
// If snippet state is not at the last tabstop, push it back on the stack
|
||||
if snippet.active_index + 1 < snippet.ranges.len() {
|
||||
self.snippet_stack.push(snippet);
|
||||
@@ -11713,6 +11870,21 @@ impl Editor {
|
||||
&& self.has_blame_entries(cx)
|
||||
}
|
||||
|
||||
pub fn render_active_line_trailer(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
self.active_line_trailer_provider
|
||||
.as_mut()?
|
||||
.render_active_line_trailer(style, &focus_handle, cx)
|
||||
}
|
||||
|
||||
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
|
||||
self.blame()
|
||||
.map_or(false, |blame| blame.read(cx).has_generated_entries())
|
||||
|
||||
@@ -279,7 +279,7 @@ pub struct EditorSettingsContent {
|
||||
|
||||
/// Whether to show the signature help pop-up after completions or bracket pairs inserted.
|
||||
///
|
||||
/// Default: true
|
||||
/// Default: false
|
||||
pub show_signature_help_after_edits: Option<bool>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
|
||||
@@ -1398,6 +1398,15 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
|
||||
});
|
||||
|
||||
// moving above start of document should move selection to start of document,
|
||||
// but the next move down should still be at the original goal_x
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "".len())]
|
||||
);
|
||||
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -1422,6 +1431,25 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
|
||||
&[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
|
||||
);
|
||||
|
||||
// moving past end of document should not change goal_x
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(5, "".len())]
|
||||
);
|
||||
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(5, "".len())]
|
||||
);
|
||||
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
|
||||
);
|
||||
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -6551,6 +6579,45 @@ async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let (text, insertion_ranges) = marked_text_ranges(
|
||||
indoc! {"
|
||||
ˇ
|
||||
"},
|
||||
false,
|
||||
);
|
||||
|
||||
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
|
||||
_ = editor.update(cx, |editor, cx| {
|
||||
let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
|
||||
|
||||
editor
|
||||
.insert_snippet(&insertion_ranges, snippet, cx)
|
||||
.unwrap();
|
||||
|
||||
fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
|
||||
let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
|
||||
assert_eq!(editor.text(cx), expected_text);
|
||||
assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
|
||||
}
|
||||
|
||||
assert(
|
||||
editor,
|
||||
cx,
|
||||
indoc! {"
|
||||
type «» =•
|
||||
"},
|
||||
);
|
||||
|
||||
assert!(editor.context_menu_visible(), "There should be a matches");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -16,8 +16,8 @@ use crate::{
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
|
||||
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
|
||||
BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
|
||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
|
||||
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
|
||||
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
@@ -34,8 +34,8 @@ use gpui::{
|
||||
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
|
||||
ViewContext, WeakView, WindowContext,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use gpui::{ClickEvent, Subscription};
|
||||
use itertools::Itertools;
|
||||
@@ -1412,7 +1412,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_blame(
|
||||
fn layout_active_line_trailer(
|
||||
&self,
|
||||
display_row: DisplayRow,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
@@ -1424,61 +1424,71 @@ impl EditorElement {
|
||||
line_height: Pixels,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
if !self
|
||||
let render_inline_blame = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.render_git_blame_inline(cx))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
.update(cx, |editor, cx| editor.render_git_blame_inline(cx));
|
||||
if render_inline_blame {
|
||||
let workspace = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.workspace
|
||||
.as_ref()
|
||||
.map(|(w, _)| w.clone());
|
||||
|
||||
let workspace = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.workspace
|
||||
.as_ref()
|
||||
.map(|(w, _)| w.clone());
|
||||
let display_point = DisplayPoint::new(display_row, 0);
|
||||
let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
|
||||
|
||||
let display_point = DisplayPoint::new(display_row, 0);
|
||||
let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
|
||||
let blame = self.editor.read(cx).blame.clone()?;
|
||||
let blame_entry = blame
|
||||
.update(cx, |blame, cx| {
|
||||
blame.blame_for_rows([Some(buffer_row)], cx).next()
|
||||
})
|
||||
.flatten()?;
|
||||
|
||||
let blame = self.editor.read(cx).blame.clone()?;
|
||||
let blame_entry = blame
|
||||
.update(cx, |blame, cx| {
|
||||
blame.blame_for_rows([Some(buffer_row)], cx).next()
|
||||
})
|
||||
.flatten()?;
|
||||
let mut element =
|
||||
render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
|
||||
|
||||
let mut element =
|
||||
render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
|
||||
let start_y = content_origin.y
|
||||
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
||||
|
||||
let start_y = content_origin.y
|
||||
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
||||
let start_x = {
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
|
||||
|
||||
let start_x = {
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
|
||||
let line_end = if let Some(crease_trailer) = crease_trailer {
|
||||
crease_trailer.bounds.right()
|
||||
} else {
|
||||
content_origin.x - scroll_pixel_position.x + line_layout.width
|
||||
};
|
||||
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
|
||||
|
||||
let line_end = if let Some(crease_trailer) = crease_trailer {
|
||||
crease_trailer.bounds.right()
|
||||
} else {
|
||||
content_origin.x - scroll_pixel_position.x + line_layout.width
|
||||
let min_column_in_pixels = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.inline_blame
|
||||
.and_then(|settings| settings.min_column)
|
||||
.map(|col| self.column_pixels(col as usize, cx))
|
||||
.unwrap_or(px(0.));
|
||||
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
|
||||
|
||||
cmp::max(padded_line_end, min_start)
|
||||
};
|
||||
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
|
||||
|
||||
let min_column_in_pixels = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.inline_blame
|
||||
.and_then(|settings| settings.min_column)
|
||||
.map(|col| self.column_pixels(col as usize, cx))
|
||||
.unwrap_or(px(0.));
|
||||
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
|
||||
let absolute_offset = point(start_x, start_y);
|
||||
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
|
||||
|
||||
cmp::max(padded_line_end, min_start)
|
||||
};
|
||||
Some(element)
|
||||
} else if let Some(mut element) = self.editor.update(cx, |editor, cx| {
|
||||
editor.render_active_line_trailer(&self.style, cx)
|
||||
}) {
|
||||
let start_y = content_origin.y
|
||||
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
||||
let start_x = content_origin.x - scroll_pixel_position.x + em_width;
|
||||
let absolute_offset = point(start_x, start_y);
|
||||
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
|
||||
|
||||
let absolute_offset = point(start_x, start_y);
|
||||
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
|
||||
|
||||
Some(element)
|
||||
Some(element)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -2019,7 +2029,7 @@ impl EditorElement {
|
||||
let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
|
||||
LineWithInvisibles::from_chunks(
|
||||
chunks,
|
||||
&style.text,
|
||||
&style,
|
||||
MAX_LINE_LEN,
|
||||
rows.len(),
|
||||
snapshot.mode,
|
||||
@@ -3454,7 +3464,7 @@ impl EditorElement {
|
||||
self.paint_lines(&invisible_display_ranges, layout, cx);
|
||||
self.paint_redactions(layout, cx);
|
||||
self.paint_cursors(layout, cx);
|
||||
self.paint_inline_blame(layout, cx);
|
||||
self.paint_active_line_trailer(layout, cx);
|
||||
cx.with_element_namespace("crease_trailers", |cx| {
|
||||
for trailer in layout.crease_trailers.iter_mut().flatten() {
|
||||
trailer.element.paint(cx);
|
||||
@@ -3936,10 +3946,10 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
if let Some(mut inline_blame) = layout.inline_blame.take() {
|
||||
fn paint_active_line_trailer(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
if let Some(mut element) = layout.active_line_trailer.take() {
|
||||
cx.paint_layer(layout.text_hitbox.bounds, |cx| {
|
||||
inline_blame.paint(cx);
|
||||
element.paint(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4372,7 +4382,7 @@ impl LineWithInvisibles {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn from_chunks<'a>(
|
||||
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
||||
text_style: &TextStyle,
|
||||
editor_style: &EditorStyle,
|
||||
max_line_len: usize,
|
||||
max_line_count: usize,
|
||||
editor_mode: EditorMode,
|
||||
@@ -4380,6 +4390,7 @@ impl LineWithInvisibles {
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<Self> {
|
||||
let text_style = &editor_style.text;
|
||||
let mut layouts = Vec::with_capacity(max_line_count);
|
||||
let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
|
||||
let mut line = String::new();
|
||||
@@ -4398,9 +4409,9 @@ impl LineWithInvisibles {
|
||||
text: "\n",
|
||||
style: None,
|
||||
is_tab: false,
|
||||
renderer: None,
|
||||
replacement: None,
|
||||
}]) {
|
||||
if let Some(renderer) = highlighted_chunk.renderer {
|
||||
if let Some(replacement) = highlighted_chunk.replacement {
|
||||
if !line.is_empty() {
|
||||
let shaped_line = cx
|
||||
.text_system()
|
||||
@@ -4413,42 +4424,71 @@ impl LineWithInvisibles {
|
||||
styles.clear();
|
||||
}
|
||||
|
||||
let available_width = if renderer.constrain_width {
|
||||
let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
|
||||
ellipsis.clone()
|
||||
} else {
|
||||
SharedString::from(Arc::from(highlighted_chunk.text))
|
||||
};
|
||||
let shaped_line = cx
|
||||
.text_system()
|
||||
.shape_line(
|
||||
chunk,
|
||||
font_size,
|
||||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||||
)
|
||||
.unwrap();
|
||||
AvailableSpace::Definite(shaped_line.width)
|
||||
} else {
|
||||
AvailableSpace::MinContent
|
||||
};
|
||||
match replacement {
|
||||
ChunkReplacement::Renderer(renderer) => {
|
||||
let available_width = if renderer.constrain_width {
|
||||
let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
|
||||
ellipsis.clone()
|
||||
} else {
|
||||
SharedString::from(Arc::from(highlighted_chunk.text))
|
||||
};
|
||||
let shaped_line = cx
|
||||
.text_system()
|
||||
.shape_line(
|
||||
chunk,
|
||||
font_size,
|
||||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||||
)
|
||||
.unwrap();
|
||||
AvailableSpace::Definite(shaped_line.width)
|
||||
} else {
|
||||
AvailableSpace::MinContent
|
||||
};
|
||||
|
||||
let mut element = (renderer.render)(&mut ChunkRendererContext {
|
||||
context: cx,
|
||||
max_width: text_width,
|
||||
});
|
||||
let line_height = text_style.line_height_in_pixels(cx.rem_size());
|
||||
let size = element.layout_as_root(
|
||||
size(available_width, AvailableSpace::Definite(line_height)),
|
||||
cx,
|
||||
);
|
||||
let mut element = (renderer.render)(&mut ChunkRendererContext {
|
||||
context: cx,
|
||||
max_width: text_width,
|
||||
});
|
||||
let line_height = text_style.line_height_in_pixels(cx.rem_size());
|
||||
let size = element.layout_as_root(
|
||||
size(available_width, AvailableSpace::Definite(line_height)),
|
||||
cx,
|
||||
);
|
||||
|
||||
width += size.width;
|
||||
len += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Element {
|
||||
element: Some(element),
|
||||
size,
|
||||
len: highlighted_chunk.text.len(),
|
||||
});
|
||||
width += size.width;
|
||||
len += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Element {
|
||||
element: Some(element),
|
||||
size,
|
||||
len: highlighted_chunk.text.len(),
|
||||
});
|
||||
}
|
||||
ChunkReplacement::Str(x) => {
|
||||
let text_style = if let Some(style) = highlighted_chunk.style {
|
||||
Cow::Owned(text_style.clone().highlight(style))
|
||||
} else {
|
||||
Cow::Borrowed(text_style)
|
||||
};
|
||||
|
||||
let run = TextRun {
|
||||
len: x.len(),
|
||||
font: text_style.font(),
|
||||
color: text_style.color,
|
||||
background_color: text_style.background_color,
|
||||
underline: text_style.underline,
|
||||
strikethrough: text_style.strikethrough,
|
||||
};
|
||||
let line_layout = cx
|
||||
.text_system()
|
||||
.shape_line(x, font_size, &[run])
|
||||
.unwrap()
|
||||
.with_len(highlighted_chunk.text.len());
|
||||
|
||||
width += line_layout.width;
|
||||
len += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Text(line_layout))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
@@ -5301,14 +5341,14 @@ impl Element for EditorElement {
|
||||
)
|
||||
});
|
||||
|
||||
let mut inline_blame = None;
|
||||
let mut active_line_trailer = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let display_row = newest_selection_head.row();
|
||||
if (start_row..end_row).contains(&display_row) {
|
||||
let line_ix = display_row.minus(start_row) as usize;
|
||||
let line_layout = &line_layouts[line_ix];
|
||||
let crease_trailer_layout = crease_trailers[line_ix].as_ref();
|
||||
inline_blame = self.layout_inline_blame(
|
||||
active_line_trailer = self.layout_active_line_trailer(
|
||||
display_row,
|
||||
&snapshot.display_snapshot,
|
||||
line_layout,
|
||||
@@ -5627,7 +5667,7 @@ impl Element for EditorElement {
|
||||
line_elements,
|
||||
line_numbers,
|
||||
blamed_display_rows,
|
||||
inline_blame,
|
||||
active_line_trailer,
|
||||
blocks,
|
||||
cursors,
|
||||
visible_cursors,
|
||||
@@ -5764,7 +5804,7 @@ pub struct EditorLayout {
|
||||
line_numbers: Vec<Option<ShapedLine>>,
|
||||
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||||
blamed_display_rows: Option<Vec<AnyElement>>,
|
||||
inline_blame: Option<AnyElement>,
|
||||
active_line_trailer: Option<AnyElement>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
@@ -5992,7 +6032,7 @@ fn layout_line(
|
||||
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
|
||||
LineWithInvisibles::from_chunks(
|
||||
chunks,
|
||||
&style.text,
|
||||
&style,
|
||||
MAX_LINE_LEN,
|
||||
1,
|
||||
snapshot.mode,
|
||||
|
||||
@@ -16,7 +16,8 @@ use gpui::{
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, DiskState, Point,
|
||||
SelectionGoal,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::AnchorRangeExt;
|
||||
@@ -635,12 +636,21 @@ impl Item for Editor {
|
||||
Some(util::truncate_and_trailoff(description, MAX_TAB_TITLE_LEN))
|
||||
});
|
||||
|
||||
// Whether the file was saved in the past but is now deleted.
|
||||
let was_deleted: bool = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.map_or(false, |file| file.disk_state() == DiskState::Deleted);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(self.title(cx).to_string())
|
||||
.color(label_color)
|
||||
.italic(params.preview),
|
||||
.italic(params.preview)
|
||||
.strikethrough(was_deleted),
|
||||
)
|
||||
.when_some(description, |this, description| {
|
||||
this.child(
|
||||
@@ -700,6 +710,10 @@ impl Item for Editor {
|
||||
self.buffer().read(cx).read(cx).is_dirty()
|
||||
}
|
||||
|
||||
fn has_deleted_file(&self, cx: &AppContext) -> bool {
|
||||
self.buffer().read(cx).read(cx).has_deleted_file()
|
||||
}
|
||||
|
||||
fn has_conflict(&self, cx: &AppContext) -> bool {
|
||||
self.buffer().read(cx).read(cx).has_conflict()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||
use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint};
|
||||
use gpui::{px, Pixels, WindowTextSystem};
|
||||
use gpui::{Pixels, WindowTextSystem};
|
||||
use language::Point;
|
||||
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
|
||||
use serde::Deserialize;
|
||||
@@ -120,7 +120,7 @@ pub(crate) fn up_by_rows(
|
||||
preserve_column_at_start: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_x = match goal {
|
||||
let goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
@@ -138,7 +138,6 @@ pub(crate) fn up_by_rows(
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = DisplayPoint::new(DisplayRow(0), 0);
|
||||
goal_x = px(0.);
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point(point, Bias::Left);
|
||||
@@ -159,7 +158,7 @@ pub(crate) fn down_by_rows(
|
||||
preserve_column_at_end: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_x = match goal {
|
||||
let goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
@@ -174,7 +173,6 @@ pub(crate) fn down_by_rows(
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = map.max_point();
|
||||
goal_x = map.x_for_display_point(point, text_layout_details)
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point(point, Bias::Right);
|
||||
@@ -610,7 +608,7 @@ mod tests {
|
||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||
Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
|
||||
};
|
||||
use gpui::{font, Context as _};
|
||||
use gpui::{font, px, Context as _};
|
||||
use language::Capability;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
@@ -977,7 +975,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 0),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
SelectionGoal::HorizontalPosition(col_2_x.0),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -990,7 +988,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 0),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
SelectionGoal::HorizontalPosition(0.0),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1059,7 +1057,7 @@ mod tests {
|
||||
let max_point_x = snapshot
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details);
|
||||
|
||||
// Can't move down off the end
|
||||
// Can't move down off the end, and attempting to do so leaves the selection goal unchanged
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
@@ -1070,7 +1068,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(7), 2),
|
||||
SelectionGoal::HorizontalPosition(max_point_x.0)
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{Context, View, ViewContext, VisualContext, WindowContext};
|
||||
use language::Language;
|
||||
@@ -7,7 +9,7 @@ use text::ToPointUtf16;
|
||||
|
||||
use crate::{
|
||||
element::register_action, lsp_ext::find_specific_language_server_in_selection, Editor,
|
||||
ExpandMacroRecursively,
|
||||
ExpandMacroRecursively, OpenDocs,
|
||||
};
|
||||
|
||||
const RUST_ANALYZER_NAME: &str = "rust-analyzer";
|
||||
@@ -24,6 +26,7 @@ pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
|
||||
.is_some()
|
||||
{
|
||||
register_action(editor, cx, expand_macro_recursively);
|
||||
register_action(editor, cx, open_docs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,3 +97,64 @@ pub fn expand_macro_recursively(
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn open_docs(editor: &mut Editor, _: &OpenDocs, cx: &mut ViewContext<'_, Editor>) {
|
||||
if editor.selections.count() == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((trigger_anchor, _rust_language, server_to_query, buffer)) =
|
||||
find_specific_language_server_in_selection(
|
||||
editor,
|
||||
cx,
|
||||
is_rust_language,
|
||||
RUST_ANALYZER_NAME,
|
||||
)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = project.clone();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
|
||||
let open_docs_task = project.update(cx, |project, cx| {
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_ext_command::OpenDocs { position },
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.spawn(|_editor, mut cx| async move {
|
||||
let docs_urls = open_docs_task.await.context("open docs")?;
|
||||
if docs_urls.is_empty() {
|
||||
log::debug!("Empty docs urls for position {position:?}");
|
||||
return Ok(());
|
||||
} else {
|
||||
log::debug!("{:?}", docs_urls);
|
||||
}
|
||||
|
||||
workspace.update(&mut cx, |_workspace, cx| {
|
||||
// Check if the local document exists, otherwise fallback to the online document.
|
||||
// Open with the default browser.
|
||||
if let Some(local_url) = docs_urls.local {
|
||||
if fs::metadata(Path::new(&local_url[8..])).is_ok() {
|
||||
cx.open_url(&local_url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(web_url) = docs_urls.web {
|
||||
cx.open_url(&web_url);
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -234,7 +234,16 @@ impl EditorLspTestContext {
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_html::language()),
|
||||
);
|
||||
)
|
||||
.with_queries(LanguageQueries {
|
||||
brackets: Some(Cow::from(indoc! {r#"
|
||||
("<" @open "/>" @close)
|
||||
("</" @open ">" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
Self::new(language, Default::default(), cx).await
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
pub mod extension_builder;
|
||||
mod extension_manifest;
|
||||
mod slash_command;
|
||||
mod types;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::lsp::LanguageServerName;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use fs::normalize_path;
|
||||
use gpui::Task;
|
||||
use language::LanguageName;
|
||||
use semantic_version::SemanticVersion;
|
||||
|
||||
pub use crate::extension_manifest::*;
|
||||
pub use crate::slash_command::*;
|
||||
pub use crate::types::*;
|
||||
|
||||
#[async_trait]
|
||||
pub trait WorktreeDelegate: Send + Sync + 'static {
|
||||
@@ -34,6 +37,43 @@ pub trait Extension: Send + Sync + 'static {
|
||||
/// Returns the path to this extension's working directory.
|
||||
fn work_dir(&self) -> Arc<Path>;
|
||||
|
||||
/// Returns a path relative to this extension's working directory.
|
||||
fn path_from_extension(&self, path: &Path) -> PathBuf {
|
||||
normalize_path(&self.work_dir().join(path))
|
||||
}
|
||||
|
||||
async fn language_server_command(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
language_name: LanguageName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Command>;
|
||||
|
||||
async fn language_server_initialization_options(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
language_name: LanguageName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>>;
|
||||
|
||||
async fn language_server_workspace_configuration(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>>;
|
||||
|
||||
async fn labels_for_completions(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
completions: Vec<Completion>,
|
||||
) -> Result<Vec<Option<CodeLabel>>>;
|
||||
|
||||
async fn labels_for_symbols(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
symbols: Vec<Symbol>,
|
||||
) -> Result<Vec<Option<CodeLabel>>>;
|
||||
|
||||
async fn complete_slash_command_argument(
|
||||
&self,
|
||||
command: SlashCommand,
|
||||
@@ -44,7 +84,7 @@ pub trait Extension: Send + Sync + 'static {
|
||||
&self,
|
||||
command: SlashCommand,
|
||||
arguments: Vec<String>,
|
||||
resource: Option<Arc<dyn WorktreeDelegate>>,
|
||||
worktree: Option<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<SlashCommandOutput>;
|
||||
|
||||
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>>;
|
||||
|
||||
49
crates/extension/src/types.rs
Normal file
49
crates/extension/src/types.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
mod lsp;
|
||||
mod slash_command;
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
pub use lsp::*;
|
||||
pub use slash_command::*;
|
||||
|
||||
/// A list of environment variables.
|
||||
pub type EnvVars = Vec<(String, String)>;
|
||||
|
||||
/// A command.
|
||||
pub struct Command {
|
||||
/// The command to execute.
|
||||
pub command: String,
|
||||
/// The arguments to pass to the command.
|
||||
pub args: Vec<String>,
|
||||
/// The environment variables to set for the command.
|
||||
pub env: EnvVars,
|
||||
}
|
||||
|
||||
/// A label containing some code.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CodeLabel {
|
||||
/// The source code to parse with Tree-sitter.
|
||||
pub code: String,
|
||||
/// The spans to display in the label.
|
||||
pub spans: Vec<CodeLabelSpan>,
|
||||
/// The range of the displayed label to include when filtering.
|
||||
pub filter_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// A span within a code label.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CodeLabelSpan {
|
||||
/// A range into the parsed code.
|
||||
CodeRange(Range<usize>),
|
||||
/// A span containing a code literal.
|
||||
Literal(CodeLabelSpanLiteral),
|
||||
}
|
||||
|
||||
/// A span containing a code literal.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CodeLabelSpanLiteral {
|
||||
/// The literal text.
|
||||
pub text: String,
|
||||
/// The name of the highlight to use for this literal.
|
||||
pub highlight_name: Option<String>,
|
||||
}
|
||||
96
crates/extension/src/types/lsp.rs
Normal file
96
crates/extension/src/types/lsp.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use std::option::Option;
|
||||
|
||||
/// An LSP completion.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Completion {
|
||||
pub label: String,
|
||||
pub label_details: Option<CompletionLabelDetails>,
|
||||
pub detail: Option<String>,
|
||||
pub kind: Option<CompletionKind>,
|
||||
pub insert_text_format: Option<InsertTextFormat>,
|
||||
}
|
||||
|
||||
/// The kind of an LSP completion.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CompletionKind {
|
||||
Text,
|
||||
Method,
|
||||
Function,
|
||||
Constructor,
|
||||
Field,
|
||||
Variable,
|
||||
Class,
|
||||
Interface,
|
||||
Module,
|
||||
Property,
|
||||
Unit,
|
||||
Value,
|
||||
Enum,
|
||||
Keyword,
|
||||
Snippet,
|
||||
Color,
|
||||
File,
|
||||
Reference,
|
||||
Folder,
|
||||
EnumMember,
|
||||
Constant,
|
||||
Struct,
|
||||
Event,
|
||||
Operator,
|
||||
TypeParameter,
|
||||
Other(i32),
|
||||
}
|
||||
|
||||
/// Label details for an LSP completion.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompletionLabelDetails {
|
||||
pub detail: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Defines how to interpret the insert text in a completion item.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum InsertTextFormat {
|
||||
PlainText,
|
||||
Snippet,
|
||||
Other(i32),
|
||||
}
|
||||
|
||||
/// An LSP symbol.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Symbol {
|
||||
pub kind: SymbolKind,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The kind of an LSP symbol.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SymbolKind {
|
||||
File,
|
||||
Module,
|
||||
Namespace,
|
||||
Package,
|
||||
Class,
|
||||
Method,
|
||||
Property,
|
||||
Field,
|
||||
Constructor,
|
||||
Enum,
|
||||
Interface,
|
||||
Function,
|
||||
Variable,
|
||||
Constant,
|
||||
String,
|
||||
Number,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
Key,
|
||||
Null,
|
||||
EnumMember,
|
||||
Struct,
|
||||
Event,
|
||||
Operator,
|
||||
TypeParameter,
|
||||
Other(i32),
|
||||
}
|
||||
@@ -8,9 +8,6 @@ keywords = ["zed", "extension"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
# Remove when we're ready to publish v0.2.0.
|
||||
publish = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ Here is the compatibility of the `zed_extension_api` with versions of Zed:
|
||||
|
||||
| Zed version | `zed_extension_api` version |
|
||||
| ----------- | --------------------------- |
|
||||
| `0.162.x` | `0.0.1` - `0.2.0` |
|
||||
| `0.149.x` | `0.0.1` - `0.1.0` |
|
||||
| `0.131.x` | `0.0.1` - `0.0.6` |
|
||||
| `0.130.x` | `0.0.1` - `0.0.5` |
|
||||
|
||||
@@ -58,3 +58,4 @@ language = { workspace = true, features = ["test-support"] }
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
reqwest_client.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -2,7 +2,10 @@ pub mod extension_lsp_adapter;
|
||||
pub mod extension_settings;
|
||||
pub mod wasm_host;
|
||||
|
||||
use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
|
||||
#[cfg(test)]
|
||||
mod extension_store_test;
|
||||
|
||||
use crate::extension_lsp_adapter::ExtensionLspAdapter;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
@@ -1235,13 +1238,9 @@ impl ExtensionStore {
|
||||
this.registration_hooks.register_lsp_adapter(
|
||||
language.clone(),
|
||||
ExtensionLspAdapter {
|
||||
extension: wasm_extension.clone(),
|
||||
host: this.wasm_host.clone(),
|
||||
extension: extension.clone(),
|
||||
language_server_id: language_server_id.clone(),
|
||||
config: wit::LanguageServerConfig {
|
||||
name: language_server_id.0.to_string(),
|
||||
language_name: language.to_string(),
|
||||
},
|
||||
language_name: language.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use crate::wasm_host::{
|
||||
wit::{self, LanguageServerConfig},
|
||||
WasmExtension, WasmHost,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use extension::WorktreeDelegate;
|
||||
use extension::{Extension, WorktreeDelegate};
|
||||
use futures::{Future, FutureExt};
|
||||
use gpui::AsyncAppContext;
|
||||
use language::{
|
||||
CodeLabel, HighlightId, Language, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
|
||||
CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, LspAdapter,
|
||||
LspAdapterDelegate,
|
||||
};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};
|
||||
use serde::Serialize;
|
||||
@@ -17,7 +14,6 @@ use serde_json::Value;
|
||||
use std::ops::Range;
|
||||
use std::{any::Any, path::PathBuf, pin::Pin, sync::Arc};
|
||||
use util::{maybe, ResultExt};
|
||||
use wasmtime_wasi::WasiView as _;
|
||||
|
||||
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
|
||||
pub struct WorktreeDelegateAdapter(pub Arc<dyn LspAdapterDelegate>);
|
||||
@@ -49,16 +45,15 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
|
||||
}
|
||||
|
||||
pub struct ExtensionLspAdapter {
|
||||
pub(crate) extension: WasmExtension,
|
||||
pub(crate) extension: Arc<dyn Extension>,
|
||||
pub(crate) language_server_id: LanguageServerName,
|
||||
pub(crate) config: LanguageServerConfig,
|
||||
pub(crate) host: Arc<WasmHost>,
|
||||
pub(crate) language_name: LanguageName,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for ExtensionLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName(self.config.name.clone().into())
|
||||
self.language_server_id.clone()
|
||||
}
|
||||
|
||||
fn get_language_server_command<'a>(
|
||||
@@ -69,33 +64,17 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
_: &'a mut AsyncAppContext,
|
||||
) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
|
||||
async move {
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
let command = self
|
||||
.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
let resource = store.data_mut().table().push(delegate)?;
|
||||
let command = extension
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&this.language_server_id,
|
||||
&this.config,
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
anyhow::Ok(command)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.language_server_command(
|
||||
self.language_server_id.clone(),
|
||||
self.language_name.clone(),
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path = self
|
||||
.host
|
||||
.path_from_extension(&self.extension.manifest.id, command.command.as_ref());
|
||||
let path = self.extension.path_from_extension(command.command.as_ref());
|
||||
|
||||
// TODO: This should now be done via the `zed::make_file_executable` function in
|
||||
// Zed extension API, but we're leaving these existing usages in place temporarily
|
||||
@@ -104,8 +83,8 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
// We can remove once the following extension versions no longer see any use:
|
||||
// - toml@0.0.2
|
||||
// - zig@0.0.1
|
||||
if ["toml", "zig"].contains(&self.extension.manifest.id.as_ref())
|
||||
&& path.starts_with(&self.host.work_dir)
|
||||
if ["toml", "zig"].contains(&self.extension.manifest().id.as_ref())
|
||||
&& path.starts_with(&self.extension.work_dir())
|
||||
{
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
@@ -153,7 +132,7 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
let code_action_kinds = self
|
||||
.extension
|
||||
.manifest
|
||||
.manifest()
|
||||
.language_servers
|
||||
.get(&self.language_server_id)
|
||||
.and_then(|server| server.code_action_kinds.clone());
|
||||
@@ -174,14 +153,14 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
//
|
||||
// We can remove once the following extension versions no longer see any use:
|
||||
// - php@0.0.1
|
||||
if self.extension.manifest.id.as_ref() == "php" {
|
||||
if self.extension.manifest().id.as_ref() == "php" {
|
||||
return HashMap::from_iter([("PHP".into(), "php".into())]);
|
||||
}
|
||||
|
||||
self.extension
|
||||
.manifest
|
||||
.manifest()
|
||||
.language_servers
|
||||
.get(&LanguageServerName(self.config.name.clone().into()))
|
||||
.get(&self.language_server_id)
|
||||
.map(|server| server.language_ids.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -190,29 +169,14 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let delegate = delegate.clone();
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
let json_options = self
|
||||
.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
let resource = store.data_mut().table().push(delegate)?;
|
||||
let options = extension
|
||||
.call_language_server_initialization_options(
|
||||
store,
|
||||
&this.language_server_id,
|
||||
&this.config,
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
anyhow::Ok(options)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.language_server_initialization_options(
|
||||
self.language_server_id.clone(),
|
||||
self.language_name.clone(),
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
Ok(if let Some(json_options) = json_options {
|
||||
serde_json::from_str(&json_options).with_context(|| {
|
||||
@@ -229,32 +193,14 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
let delegate = delegate.clone();
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
let json_options: Option<String> = self
|
||||
.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
let resource = store.data_mut().table().push(delegate)?;
|
||||
let options = extension
|
||||
.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&this.language_server_id,
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
anyhow::Ok(options)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.language_server_workspace_configuration(self.language_server_id.clone(), delegate)
|
||||
.await?;
|
||||
Ok(if let Some(json_options) = json_options {
|
||||
serde_json::from_str(&json_options).with_context(|| {
|
||||
format!("failed to parse initialization_options from extension: {json_options}")
|
||||
format!("failed to parse workspace_configuration from extension: {json_options}")
|
||||
})?
|
||||
} else {
|
||||
serde_json::json!({})
|
||||
@@ -268,30 +214,16 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
let completions = completions
|
||||
.iter()
|
||||
.map(|completion| wit::Completion::from(completion.clone()))
|
||||
.cloned()
|
||||
.map(lsp_completion_to_extension)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let labels = self
|
||||
.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
extension
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&this.language_server_id,
|
||||
completions,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.labels_for_completions(self.language_server_id.clone(), completions)
|
||||
.await?;
|
||||
|
||||
Ok(labels_from_wit(labels, language))
|
||||
Ok(labels_from_extension(labels, language))
|
||||
}
|
||||
|
||||
async fn labels_for_symbols(
|
||||
@@ -302,34 +234,29 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
let symbols = symbols
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(name, kind)| wit::Symbol {
|
||||
.map(|(name, kind)| extension::Symbol {
|
||||
name,
|
||||
kind: kind.into(),
|
||||
kind: lsp_symbol_kind_to_extension(kind),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let labels = self
|
||||
.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
extension
|
||||
.call_labels_for_symbols(store, &this.language_server_id, symbols)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.labels_for_symbols(self.language_server_id.clone(), symbols)
|
||||
.await?;
|
||||
|
||||
Ok(labels_from_wit(labels, language))
|
||||
Ok(labels_from_extension(
|
||||
labels
|
||||
.into_iter()
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect(),
|
||||
language,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn labels_from_wit(
|
||||
labels: Vec<Option<wit::CodeLabel>>,
|
||||
fn labels_from_extension(
|
||||
labels: Vec<Option<extension::CodeLabel>>,
|
||||
language: &Arc<Language>,
|
||||
) -> Vec<Option<CodeLabel>> {
|
||||
labels
|
||||
@@ -347,7 +274,7 @@ fn labels_from_wit(
|
||||
}
|
||||
|
||||
fn build_code_label(
|
||||
label: &wit::CodeLabel,
|
||||
label: &extension::CodeLabel,
|
||||
parsed_runs: &[(Range<usize>, HighlightId)],
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
@@ -356,8 +283,7 @@ fn build_code_label(
|
||||
|
||||
for span in &label.spans {
|
||||
match span {
|
||||
wit::CodeLabelSpan::CodeRange(range) => {
|
||||
let range = Range::from(*range);
|
||||
extension::CodeLabelSpan::CodeRange(range) => {
|
||||
let code_span = &label.code.get(range.clone())?;
|
||||
let mut input_ix = range.start;
|
||||
let mut output_ix = text.len();
|
||||
@@ -383,7 +309,7 @@ fn build_code_label(
|
||||
|
||||
text.push_str(code_span);
|
||||
}
|
||||
wit::CodeLabelSpan::Literal(span) => {
|
||||
extension::CodeLabelSpan::Literal(span) => {
|
||||
let highlight_id = language
|
||||
.grammar()
|
||||
.zip(span.highlight_name.as_ref())
|
||||
@@ -398,7 +324,7 @@ fn build_code_label(
|
||||
}
|
||||
}
|
||||
|
||||
let filter_range = Range::from(label.filter_range);
|
||||
let filter_range = label.filter_range.clone();
|
||||
text.get(filter_range.clone())?;
|
||||
Some(CodeLabel {
|
||||
text,
|
||||
@@ -407,109 +333,101 @@ fn build_code_label(
|
||||
})
|
||||
}
|
||||
|
||||
impl From<wit::Range> for Range<usize> {
|
||||
fn from(range: wit::Range) -> Self {
|
||||
let start = range.start as usize;
|
||||
let end = range.end as usize;
|
||||
start..end
|
||||
fn lsp_completion_to_extension(value: lsp::CompletionItem) -> extension::Completion {
|
||||
extension::Completion {
|
||||
label: value.label,
|
||||
label_details: value
|
||||
.label_details
|
||||
.map(lsp_completion_item_label_details_to_extension),
|
||||
detail: value.detail,
|
||||
kind: value.kind.map(lsp_completion_item_kind_to_extension),
|
||||
insert_text_format: value
|
||||
.insert_text_format
|
||||
.map(lsp_insert_text_format_to_extension),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::CompletionItem> for wit::Completion {
|
||||
fn from(value: lsp::CompletionItem) -> Self {
|
||||
Self {
|
||||
label: value.label,
|
||||
label_details: value.label_details.map(Into::into),
|
||||
detail: value.detail,
|
||||
kind: value.kind.map(Into::into),
|
||||
insert_text_format: value.insert_text_format.map(Into::into),
|
||||
}
|
||||
fn lsp_completion_item_label_details_to_extension(
|
||||
value: lsp::CompletionItemLabelDetails,
|
||||
) -> extension::CompletionLabelDetails {
|
||||
extension::CompletionLabelDetails {
|
||||
detail: value.detail,
|
||||
description: value.description,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::CompletionItemLabelDetails> for wit::CompletionLabelDetails {
|
||||
fn from(value: lsp::CompletionItemLabelDetails) -> Self {
|
||||
Self {
|
||||
detail: value.detail,
|
||||
description: value.description,
|
||||
}
|
||||
fn lsp_completion_item_kind_to_extension(
|
||||
value: lsp::CompletionItemKind,
|
||||
) -> extension::CompletionKind {
|
||||
match value {
|
||||
lsp::CompletionItemKind::TEXT => extension::CompletionKind::Text,
|
||||
lsp::CompletionItemKind::METHOD => extension::CompletionKind::Method,
|
||||
lsp::CompletionItemKind::FUNCTION => extension::CompletionKind::Function,
|
||||
lsp::CompletionItemKind::CONSTRUCTOR => extension::CompletionKind::Constructor,
|
||||
lsp::CompletionItemKind::FIELD => extension::CompletionKind::Field,
|
||||
lsp::CompletionItemKind::VARIABLE => extension::CompletionKind::Variable,
|
||||
lsp::CompletionItemKind::CLASS => extension::CompletionKind::Class,
|
||||
lsp::CompletionItemKind::INTERFACE => extension::CompletionKind::Interface,
|
||||
lsp::CompletionItemKind::MODULE => extension::CompletionKind::Module,
|
||||
lsp::CompletionItemKind::PROPERTY => extension::CompletionKind::Property,
|
||||
lsp::CompletionItemKind::UNIT => extension::CompletionKind::Unit,
|
||||
lsp::CompletionItemKind::VALUE => extension::CompletionKind::Value,
|
||||
lsp::CompletionItemKind::ENUM => extension::CompletionKind::Enum,
|
||||
lsp::CompletionItemKind::KEYWORD => extension::CompletionKind::Keyword,
|
||||
lsp::CompletionItemKind::SNIPPET => extension::CompletionKind::Snippet,
|
||||
lsp::CompletionItemKind::COLOR => extension::CompletionKind::Color,
|
||||
lsp::CompletionItemKind::FILE => extension::CompletionKind::File,
|
||||
lsp::CompletionItemKind::REFERENCE => extension::CompletionKind::Reference,
|
||||
lsp::CompletionItemKind::FOLDER => extension::CompletionKind::Folder,
|
||||
lsp::CompletionItemKind::ENUM_MEMBER => extension::CompletionKind::EnumMember,
|
||||
lsp::CompletionItemKind::CONSTANT => extension::CompletionKind::Constant,
|
||||
lsp::CompletionItemKind::STRUCT => extension::CompletionKind::Struct,
|
||||
lsp::CompletionItemKind::EVENT => extension::CompletionKind::Event,
|
||||
lsp::CompletionItemKind::OPERATOR => extension::CompletionKind::Operator,
|
||||
lsp::CompletionItemKind::TYPE_PARAMETER => extension::CompletionKind::TypeParameter,
|
||||
_ => extension::CompletionKind::Other(extract_int(value)),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::CompletionItemKind> for wit::CompletionKind {
|
||||
fn from(value: lsp::CompletionItemKind) -> Self {
|
||||
match value {
|
||||
lsp::CompletionItemKind::TEXT => Self::Text,
|
||||
lsp::CompletionItemKind::METHOD => Self::Method,
|
||||
lsp::CompletionItemKind::FUNCTION => Self::Function,
|
||||
lsp::CompletionItemKind::CONSTRUCTOR => Self::Constructor,
|
||||
lsp::CompletionItemKind::FIELD => Self::Field,
|
||||
lsp::CompletionItemKind::VARIABLE => Self::Variable,
|
||||
lsp::CompletionItemKind::CLASS => Self::Class,
|
||||
lsp::CompletionItemKind::INTERFACE => Self::Interface,
|
||||
lsp::CompletionItemKind::MODULE => Self::Module,
|
||||
lsp::CompletionItemKind::PROPERTY => Self::Property,
|
||||
lsp::CompletionItemKind::UNIT => Self::Unit,
|
||||
lsp::CompletionItemKind::VALUE => Self::Value,
|
||||
lsp::CompletionItemKind::ENUM => Self::Enum,
|
||||
lsp::CompletionItemKind::KEYWORD => Self::Keyword,
|
||||
lsp::CompletionItemKind::SNIPPET => Self::Snippet,
|
||||
lsp::CompletionItemKind::COLOR => Self::Color,
|
||||
lsp::CompletionItemKind::FILE => Self::File,
|
||||
lsp::CompletionItemKind::REFERENCE => Self::Reference,
|
||||
lsp::CompletionItemKind::FOLDER => Self::Folder,
|
||||
lsp::CompletionItemKind::ENUM_MEMBER => Self::EnumMember,
|
||||
lsp::CompletionItemKind::CONSTANT => Self::Constant,
|
||||
lsp::CompletionItemKind::STRUCT => Self::Struct,
|
||||
lsp::CompletionItemKind::EVENT => Self::Event,
|
||||
lsp::CompletionItemKind::OPERATOR => Self::Operator,
|
||||
lsp::CompletionItemKind::TYPE_PARAMETER => Self::TypeParameter,
|
||||
_ => Self::Other(extract_int(value)),
|
||||
}
|
||||
fn lsp_insert_text_format_to_extension(
|
||||
value: lsp::InsertTextFormat,
|
||||
) -> extension::InsertTextFormat {
|
||||
match value {
|
||||
lsp::InsertTextFormat::PLAIN_TEXT => extension::InsertTextFormat::PlainText,
|
||||
lsp::InsertTextFormat::SNIPPET => extension::InsertTextFormat::Snippet,
|
||||
_ => extension::InsertTextFormat::Other(extract_int(value)),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::InsertTextFormat> for wit::InsertTextFormat {
|
||||
fn from(value: lsp::InsertTextFormat) -> Self {
|
||||
match value {
|
||||
lsp::InsertTextFormat::PLAIN_TEXT => Self::PlainText,
|
||||
lsp::InsertTextFormat::SNIPPET => Self::Snippet,
|
||||
_ => Self::Other(extract_int(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::SymbolKind> for wit::SymbolKind {
|
||||
fn from(value: lsp::SymbolKind) -> Self {
|
||||
match value {
|
||||
lsp::SymbolKind::FILE => Self::File,
|
||||
lsp::SymbolKind::MODULE => Self::Module,
|
||||
lsp::SymbolKind::NAMESPACE => Self::Namespace,
|
||||
lsp::SymbolKind::PACKAGE => Self::Package,
|
||||
lsp::SymbolKind::CLASS => Self::Class,
|
||||
lsp::SymbolKind::METHOD => Self::Method,
|
||||
lsp::SymbolKind::PROPERTY => Self::Property,
|
||||
lsp::SymbolKind::FIELD => Self::Field,
|
||||
lsp::SymbolKind::CONSTRUCTOR => Self::Constructor,
|
||||
lsp::SymbolKind::ENUM => Self::Enum,
|
||||
lsp::SymbolKind::INTERFACE => Self::Interface,
|
||||
lsp::SymbolKind::FUNCTION => Self::Function,
|
||||
lsp::SymbolKind::VARIABLE => Self::Variable,
|
||||
lsp::SymbolKind::CONSTANT => Self::Constant,
|
||||
lsp::SymbolKind::STRING => Self::String,
|
||||
lsp::SymbolKind::NUMBER => Self::Number,
|
||||
lsp::SymbolKind::BOOLEAN => Self::Boolean,
|
||||
lsp::SymbolKind::ARRAY => Self::Array,
|
||||
lsp::SymbolKind::OBJECT => Self::Object,
|
||||
lsp::SymbolKind::KEY => Self::Key,
|
||||
lsp::SymbolKind::NULL => Self::Null,
|
||||
lsp::SymbolKind::ENUM_MEMBER => Self::EnumMember,
|
||||
lsp::SymbolKind::STRUCT => Self::Struct,
|
||||
lsp::SymbolKind::EVENT => Self::Event,
|
||||
lsp::SymbolKind::OPERATOR => Self::Operator,
|
||||
lsp::SymbolKind::TYPE_PARAMETER => Self::TypeParameter,
|
||||
_ => Self::Other(extract_int(value)),
|
||||
}
|
||||
fn lsp_symbol_kind_to_extension(value: lsp::SymbolKind) -> extension::SymbolKind {
|
||||
match value {
|
||||
lsp::SymbolKind::FILE => extension::SymbolKind::File,
|
||||
lsp::SymbolKind::MODULE => extension::SymbolKind::Module,
|
||||
lsp::SymbolKind::NAMESPACE => extension::SymbolKind::Namespace,
|
||||
lsp::SymbolKind::PACKAGE => extension::SymbolKind::Package,
|
||||
lsp::SymbolKind::CLASS => extension::SymbolKind::Class,
|
||||
lsp::SymbolKind::METHOD => extension::SymbolKind::Method,
|
||||
lsp::SymbolKind::PROPERTY => extension::SymbolKind::Property,
|
||||
lsp::SymbolKind::FIELD => extension::SymbolKind::Field,
|
||||
lsp::SymbolKind::CONSTRUCTOR => extension::SymbolKind::Constructor,
|
||||
lsp::SymbolKind::ENUM => extension::SymbolKind::Enum,
|
||||
lsp::SymbolKind::INTERFACE => extension::SymbolKind::Interface,
|
||||
lsp::SymbolKind::FUNCTION => extension::SymbolKind::Function,
|
||||
lsp::SymbolKind::VARIABLE => extension::SymbolKind::Variable,
|
||||
lsp::SymbolKind::CONSTANT => extension::SymbolKind::Constant,
|
||||
lsp::SymbolKind::STRING => extension::SymbolKind::String,
|
||||
lsp::SymbolKind::NUMBER => extension::SymbolKind::Number,
|
||||
lsp::SymbolKind::BOOLEAN => extension::SymbolKind::Boolean,
|
||||
lsp::SymbolKind::ARRAY => extension::SymbolKind::Array,
|
||||
lsp::SymbolKind::OBJECT => extension::SymbolKind::Object,
|
||||
lsp::SymbolKind::KEY => extension::SymbolKind::Key,
|
||||
lsp::SymbolKind::NULL => extension::SymbolKind::Null,
|
||||
lsp::SymbolKind::ENUM_MEMBER => extension::SymbolKind::EnumMember,
|
||||
lsp::SymbolKind::STRUCT => extension::SymbolKind::Struct,
|
||||
lsp::SymbolKind::EVENT => extension::SymbolKind::Event,
|
||||
lsp::SymbolKind::OPERATOR => extension::SymbolKind::Operator,
|
||||
lsp::SymbolKind::TYPE_PARAMETER => extension::SymbolKind::TypeParameter,
|
||||
_ => extension::SymbolKind::Other(extract_int(value)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,21 +454,14 @@ fn test_build_code_label() {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let label = build_code_label(
|
||||
&wit::CodeLabel {
|
||||
&extension::CodeLabel {
|
||||
spans: vec![
|
||||
wit::CodeLabelSpan::CodeRange(wit::Range {
|
||||
start: code.find("pqrs").unwrap() as u32,
|
||||
end: code.len() as u32,
|
||||
}),
|
||||
wit::CodeLabelSpan::CodeRange(wit::Range {
|
||||
start: code.find(": fn").unwrap() as u32,
|
||||
end: code.find(" = ").unwrap() as u32,
|
||||
}),
|
||||
extension::CodeLabelSpan::CodeRange(code.find("pqrs").unwrap()..code.len()),
|
||||
extension::CodeLabelSpan::CodeRange(
|
||||
code.find(": fn").unwrap()..code.find(" = ").unwrap(),
|
||||
),
|
||||
],
|
||||
filter_range: wit::Range {
|
||||
start: 0,
|
||||
end: "pqrs.tuv".len() as u32,
|
||||
},
|
||||
filter_range: 0.."pqrs.tuv".len(),
|
||||
code,
|
||||
},
|
||||
&code_runs,
|
||||
@@ -588,21 +499,14 @@ fn test_build_code_label_with_invalid_ranges() {
|
||||
// A span uses a code range that is invalid because it starts inside of
|
||||
// a multi-byte character.
|
||||
let label = build_code_label(
|
||||
&wit::CodeLabel {
|
||||
&extension::CodeLabel {
|
||||
spans: vec![
|
||||
wit::CodeLabelSpan::CodeRange(wit::Range {
|
||||
start: code.find('B').unwrap() as u32,
|
||||
end: code.find(" = ").unwrap() as u32,
|
||||
}),
|
||||
wit::CodeLabelSpan::CodeRange(wit::Range {
|
||||
start: code.find('🏀').unwrap() as u32 + 1,
|
||||
end: code.len() as u32,
|
||||
}),
|
||||
extension::CodeLabelSpan::CodeRange(
|
||||
code.find('B').unwrap()..code.find(" = ").unwrap(),
|
||||
),
|
||||
extension::CodeLabelSpan::CodeRange((code.find('🏀').unwrap() + 1)..code.len()),
|
||||
],
|
||||
filter_range: wit::Range {
|
||||
start: 0,
|
||||
end: "B".len() as u32,
|
||||
},
|
||||
filter_range: 0.."B".len(),
|
||||
code,
|
||||
},
|
||||
&code_runs,
|
||||
@@ -612,12 +516,14 @@ fn test_build_code_label_with_invalid_ranges() {
|
||||
|
||||
// Filter range extends beyond actual text
|
||||
let label = build_code_label(
|
||||
&wit::CodeLabel {
|
||||
spans: vec![wit::CodeLabelSpan::Literal(wit::CodeLabelSpanLiteral {
|
||||
text: "abc".into(),
|
||||
highlight_name: Some("type".into()),
|
||||
})],
|
||||
filter_range: wit::Range { start: 0, end: 5 },
|
||||
&extension::CodeLabel {
|
||||
spans: vec![extension::CodeLabelSpan::Literal(
|
||||
extension::CodeLabelSpanLiteral {
|
||||
text: "abc".into(),
|
||||
highlight_name: Some("type".into()),
|
||||
},
|
||||
)],
|
||||
filter_range: 0..5,
|
||||
code: String::new(),
|
||||
},
|
||||
&code_runs,
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use crate::extension_lsp_adapter::ExtensionLspAdapter;
|
||||
use crate::{
|
||||
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
|
||||
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionSettings, ExtensionStore,
|
||||
GrammarManifestEntry, SchemaVersion, RELOAD_DEBOUNCE_DURATION,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use context_servers::ContextServerFactoryRegistry;
|
||||
use extension_host::ExtensionSettings;
|
||||
use extension_host::SchemaVersion;
|
||||
use extension_host::{
|
||||
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
|
||||
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
|
||||
RELOAD_DEBOUNCE_DURATION,
|
||||
};
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
||||
use gpui::{Context, SemanticVersion, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
@@ -23,7 +20,6 @@ use release_channel::AppVersion;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use serde_json::json;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use snippet_provider::SnippetRegistry;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
@@ -32,6 +28,84 @@ use std::{
|
||||
use theme::ThemeRegistry;
|
||||
use util::test::temp_tree;
|
||||
|
||||
use crate::ExtensionRegistrationHooks;
|
||||
|
||||
struct TestExtensionRegistrationHooks {
|
||||
executor: BackgroundExecutor,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
}
|
||||
|
||||
impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks {
|
||||
fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
|
||||
self.executor.spawn(async move {
|
||||
let themes = theme::read_user_theme(&path, fs).await?;
|
||||
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
|
||||
let theme_registry = self.theme_registry.clone();
|
||||
self.executor
|
||||
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
|
||||
}
|
||||
|
||||
fn remove_user_themes(&self, themes: Vec<SharedString>) {
|
||||
self.theme_registry.remove_user_themes(&themes);
|
||||
}
|
||||
|
||||
fn register_language(
|
||||
&self,
|
||||
language: language::LanguageName,
|
||||
grammar: Option<Arc<str>>,
|
||||
matcher: language::LanguageMatcher,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_language(language, grammar, matcher, load)
|
||||
}
|
||||
|
||||
fn remove_languages(
|
||||
&self,
|
||||
languages_to_remove: &[language::LanguageName],
|
||||
grammars_to_remove: &[Arc<str>],
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_languages(&languages_to_remove, &grammars_to_remove);
|
||||
}
|
||||
|
||||
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
|
||||
self.language_registry.register_wasm_grammars(grammars)
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
language_name: language::LanguageName,
|
||||
adapter: ExtensionLspAdapter,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language_name, Arc::new(adapter));
|
||||
}
|
||||
|
||||
fn update_lsp_status(
|
||||
&self,
|
||||
server_name: lsp::LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
) {
|
||||
self.language_registry
|
||||
.update_lsp_status(server_name, status);
|
||||
}
|
||||
|
||||
fn remove_lsp_adapter(
|
||||
&self,
|
||||
language_name: &language::LanguageName,
|
||||
server_name: &lsp::LanguageServerName,
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_lsp_adapter(language_name, server_name);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
@@ -265,27 +339,18 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
|
||||
let snippet_registry = Arc::new(SnippetRegistry::new());
|
||||
let context_server_factory_registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
|
||||
let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
|
||||
executor: cx.executor(),
|
||||
language_registry: language_registry.clone(),
|
||||
theme_registry: theme_registry.clone(),
|
||||
});
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let store = cx.new_model(|cx| {
|
||||
let extension_registration_hooks = crate::ConcreteExtensionRegistrationHooks::new(
|
||||
theme_registry.clone(),
|
||||
slash_command_registry.clone(),
|
||||
indexed_docs_registry.clone(),
|
||||
snippet_registry.clone(),
|
||||
language_registry.clone(),
|
||||
context_server_factory_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
ExtensionStore::new(
|
||||
PathBuf::from("/the-extension-dir"),
|
||||
None,
|
||||
extension_registration_hooks,
|
||||
registration_hooks.clone(),
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
http_client.clone(),
|
||||
@@ -407,20 +472,10 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
// Create new extension store, as if Zed were restarting.
|
||||
drop(store);
|
||||
let store = cx.new_model(|cx| {
|
||||
let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
|
||||
theme_registry.clone(),
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry.clone(),
|
||||
context_server_factory_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
ExtensionStore::new(
|
||||
PathBuf::from("/the-extension-dir"),
|
||||
None,
|
||||
extension_api,
|
||||
registration_hooks,
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
http_client.clone(),
|
||||
@@ -505,10 +560,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
|
||||
let snippet_registry = Arc::new(SnippetRegistry::new());
|
||||
let context_server_factory_registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
|
||||
let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
|
||||
executor: cx.executor(),
|
||||
language_registry: language_registry.clone(),
|
||||
theme_registry: theme_registry.clone(),
|
||||
});
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let mut status_updates = language_registry.language_server_binary_statuses();
|
||||
@@ -599,19 +655,10 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
|
||||
|
||||
let extension_store = cx.new_model(|cx| {
|
||||
let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
|
||||
theme_registry.clone(),
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry.clone(),
|
||||
context_server_factory_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
ExtensionStore::new(
|
||||
extensions_dir.clone(),
|
||||
Some(cache_dir),
|
||||
extension_api,
|
||||
registration_hooks,
|
||||
fs.clone(),
|
||||
extension_client.clone(),
|
||||
builder_client,
|
||||
@@ -626,7 +673,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
let executor = cx.executor();
|
||||
let _task = cx.executor().spawn(async move {
|
||||
while let Some(event) = events.next().await {
|
||||
if let extension_host::Event::StartedReloading = event {
|
||||
if let Event::StartedReloading = event {
|
||||
executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ use crate::{ExtensionManifest, ExtensionRegistrationHooks};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use extension::{
|
||||
KeyValueStoreDelegate, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput,
|
||||
WorktreeDelegate,
|
||||
CodeLabel, Command, Completion, KeyValueStoreDelegate, SlashCommand,
|
||||
SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
|
||||
};
|
||||
use fs::{normalize_path, Fs};
|
||||
use futures::future::LocalBoxFuture;
|
||||
@@ -19,6 +19,8 @@ use futures::{
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
|
||||
use http_client::HttpClient;
|
||||
use language::LanguageName;
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use release_channel::ReleaseChannel;
|
||||
use semantic_version::SemanticVersion;
|
||||
@@ -65,6 +67,132 @@ impl extension::Extension for WasmExtension {
|
||||
self.work_dir.clone()
|
||||
}
|
||||
|
||||
async fn language_server_command(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
language_name: LanguageName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Command> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(worktree)?;
|
||||
let command = extension
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&language_server_id,
|
||||
&language_name,
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
|
||||
Ok(command.into())
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn language_server_initialization_options(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
language_name: LanguageName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(worktree)?;
|
||||
let options = extension
|
||||
.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id,
|
||||
&language_name,
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
anyhow::Ok(options)
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn language_server_workspace_configuration(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(worktree)?;
|
||||
let options = extension
|
||||
.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id,
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
anyhow::Ok(options)
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn labels_for_completions(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
completions: Vec<Completion>,
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let labels = extension
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id,
|
||||
completions.into_iter().map(Into::into).collect(),
|
||||
)
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
|
||||
Ok(labels
|
||||
.into_iter()
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect())
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn labels_for_symbols(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
symbols: Vec<Symbol>,
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let labels = extension
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id,
|
||||
symbols.into_iter().map(Into::into).collect(),
|
||||
)
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
|
||||
Ok(labels
|
||||
.into_iter()
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect())
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn complete_slash_command_argument(
|
||||
&self,
|
||||
command: SlashCommand,
|
||||
@@ -255,7 +383,7 @@ impl WasmHost {
|
||||
|
||||
Ok(WasmExtension {
|
||||
manifest: manifest.clone(),
|
||||
work_dir: this.work_dir.clone().into(),
|
||||
work_dir: this.work_dir.join(manifest.id.as_ref()).into(),
|
||||
tx,
|
||||
zed_api_version,
|
||||
})
|
||||
@@ -286,11 +414,6 @@ impl WasmHost {
|
||||
.build())
|
||||
}
|
||||
|
||||
pub fn path_from_extension(&self, id: &Arc<str>, path: &Path) -> PathBuf {
|
||||
let extension_work_dir = self.work_dir.join(id.as_ref());
|
||||
normalize_path(&extension_work_dir.join(path))
|
||||
}
|
||||
|
||||
pub fn writeable_path_from_extension(&self, id: &Arc<str>, path: &Path) -> Result<PathBuf> {
|
||||
let extension_work_dir = self.work_dir.join(id.as_ref());
|
||||
let path = normalize_path(&extension_work_dir.join(path));
|
||||
|
||||
@@ -4,6 +4,7 @@ mod since_v0_0_6;
|
||||
mod since_v0_1_0;
|
||||
mod since_v0_2_0;
|
||||
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
|
||||
use language::LanguageName;
|
||||
use lsp::LanguageServerName;
|
||||
use release_channel::ReleaseChannel;
|
||||
use since_v0_2_0 as latest;
|
||||
@@ -57,12 +58,35 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive
|
||||
|
||||
let max_version = match release_channel {
|
||||
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
|
||||
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_1_0::MAX_VERSION,
|
||||
ReleaseChannel::Stable | ReleaseChannel::Preview => latest::MAX_VERSION,
|
||||
};
|
||||
|
||||
since_v0_0_1::MIN_VERSION..=max_version
|
||||
}
|
||||
|
||||
/// Authorizes access to use unreleased versions of the Wasm API, based on the provided [`ReleaseChannel`].
|
||||
///
|
||||
/// Note: If there isn't currently an unreleased Wasm API version this function may be unused. Don't delete it!
|
||||
pub fn authorize_access_to_unreleased_wasm_api_version(
|
||||
release_channel: ReleaseChannel,
|
||||
) -> Result<()> {
|
||||
let allow_unreleased_version = match release_channel {
|
||||
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
|
||||
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
||||
// We always allow the latest in tests so that the extension tests pass on release branches.
|
||||
cfg!(any(test, feature = "test-support"))
|
||||
}
|
||||
};
|
||||
|
||||
if !allow_unreleased_version {
|
||||
Err(anyhow!(
|
||||
"unreleased versions of the extension API can only be used on development builds of Zed"
|
||||
))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub enum Extension {
|
||||
V020(since_v0_2_0::Extension),
|
||||
V010(since_v0_1_0::Extension),
|
||||
@@ -78,20 +102,10 @@ impl Extension {
|
||||
version: SemanticVersion,
|
||||
component: &Component,
|
||||
) -> Result<Self> {
|
||||
// Note: The release channel can be used to stage a new version of the extension API.
|
||||
let _ = release_channel;
|
||||
|
||||
if version >= latest::MIN_VERSION {
|
||||
// Note: The release channel can be used to stage a new version of the extension API.
|
||||
// We always allow the latest in tests so that the extension tests pass on release branches.
|
||||
let allow_latest_version = match release_channel {
|
||||
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
|
||||
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
||||
cfg!(any(test, feature = "test-support"))
|
||||
}
|
||||
};
|
||||
if !allow_latest_version {
|
||||
Err(anyhow!(
|
||||
"unreleased versions of the extension API can only be used on development builds of Zed"
|
||||
))?;
|
||||
}
|
||||
let extension =
|
||||
latest::Extension::instantiate_async(store, component, latest::linker())
|
||||
.await
|
||||
@@ -150,7 +164,7 @@ impl Extension {
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
config: &LanguageServerConfig,
|
||||
language_name: &LanguageName,
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
@@ -167,11 +181,26 @@ impl Extension {
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V004(ext) => Ok(ext
|
||||
.call_language_server_command(store, config, resource)
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
name: language_server_id.0.to_string(),
|
||||
language_name: language_name.to_string(),
|
||||
},
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V001(ext) => Ok(ext
|
||||
.call_language_server_command(store, &config.clone().into(), resource)
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
name: language_server_id.0.to_string(),
|
||||
language_name: language_name.to_string(),
|
||||
}
|
||||
.into(),
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
}
|
||||
@@ -181,7 +210,7 @@ impl Extension {
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
config: &LanguageServerConfig,
|
||||
language_name: &LanguageName,
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
@@ -210,13 +239,24 @@ impl Extension {
|
||||
.await
|
||||
}
|
||||
Extension::V004(ext) => {
|
||||
ext.call_language_server_initialization_options(store, config, resource)
|
||||
.await
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
name: language_server_id.0.to_string(),
|
||||
language_name: language_name.to_string(),
|
||||
},
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V001(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&config.clone().into(),
|
||||
&LanguageServerConfig {
|
||||
name: language_server_id.0.to_string(),
|
||||
language_name: language_name.to_string(),
|
||||
}
|
||||
.into(),
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -22,7 +22,6 @@ use wasmtime::component::{Linker, Resource};
|
||||
use super::latest;
|
||||
|
||||
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0);
|
||||
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::wasm_host::wit::since_v0_2_0::slash_command::SlashCommandOutputSection;
|
||||
use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind};
|
||||
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
|
||||
use ::http_client::{AsyncBody, HttpRequestExt};
|
||||
use ::settings::{Settings, WorktreeId};
|
||||
@@ -55,6 +56,159 @@ pub fn linker() -> &'static Linker<WasmState> {
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
}
|
||||
|
||||
impl From<Range> for std::ops::Range<usize> {
|
||||
fn from(range: Range) -> Self {
|
||||
let start = range.start as usize;
|
||||
let end = range.end as usize;
|
||||
start..end
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Command> for extension::Command {
|
||||
fn from(value: Command) -> Self {
|
||||
Self {
|
||||
command: value.command,
|
||||
args: value.args,
|
||||
env: value.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CodeLabel> for extension::CodeLabel {
|
||||
fn from(value: CodeLabel) -> Self {
|
||||
Self {
|
||||
code: value.code,
|
||||
spans: value.spans.into_iter().map(Into::into).collect(),
|
||||
filter_range: value.filter_range.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CodeLabelSpan> for extension::CodeLabelSpan {
|
||||
fn from(value: CodeLabelSpan) -> Self {
|
||||
match value {
|
||||
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
|
||||
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
|
||||
fn from(value: CodeLabelSpanLiteral) -> Self {
|
||||
Self {
|
||||
text: value.text,
|
||||
highlight_name: value.highlight_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<extension::Completion> for Completion {
|
||||
fn from(value: extension::Completion) -> Self {
|
||||
Self {
|
||||
label: value.label,
|
||||
label_details: value.label_details.map(Into::into),
|
||||
detail: value.detail,
|
||||
kind: value.kind.map(Into::into),
|
||||
insert_text_format: value.insert_text_format.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<extension::CompletionLabelDetails> for CompletionLabelDetails {
|
||||
fn from(value: extension::CompletionLabelDetails) -> Self {
|
||||
Self {
|
||||
detail: value.detail,
|
||||
description: value.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<extension::CompletionKind> for CompletionKind {
|
||||
fn from(value: extension::CompletionKind) -> Self {
|
||||
match value {
|
||||
extension::CompletionKind::Text => Self::Text,
|
||||
extension::CompletionKind::Method => Self::Method,
|
||||
extension::CompletionKind::Function => Self::Function,
|
||||
extension::CompletionKind::Constructor => Self::Constructor,
|
||||
extension::CompletionKind::Field => Self::Field,
|
||||
extension::CompletionKind::Variable => Self::Variable,
|
||||
extension::CompletionKind::Class => Self::Class,
|
||||
extension::CompletionKind::Interface => Self::Interface,
|
||||
extension::CompletionKind::Module => Self::Module,
|
||||
extension::CompletionKind::Property => Self::Property,
|
||||
extension::CompletionKind::Unit => Self::Unit,
|
||||
extension::CompletionKind::Value => Self::Value,
|
||||
extension::CompletionKind::Enum => Self::Enum,
|
||||
extension::CompletionKind::Keyword => Self::Keyword,
|
||||
extension::CompletionKind::Snippet => Self::Snippet,
|
||||
extension::CompletionKind::Color => Self::Color,
|
||||
extension::CompletionKind::File => Self::File,
|
||||
extension::CompletionKind::Reference => Self::Reference,
|
||||
extension::CompletionKind::Folder => Self::Folder,
|
||||
extension::CompletionKind::EnumMember => Self::EnumMember,
|
||||
extension::CompletionKind::Constant => Self::Constant,
|
||||
extension::CompletionKind::Struct => Self::Struct,
|
||||
extension::CompletionKind::Event => Self::Event,
|
||||
extension::CompletionKind::Operator => Self::Operator,
|
||||
extension::CompletionKind::TypeParameter => Self::TypeParameter,
|
||||
extension::CompletionKind::Other(value) => Self::Other(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<extension::InsertTextFormat> for InsertTextFormat {
|
||||
fn from(value: extension::InsertTextFormat) -> Self {
|
||||
match value {
|
||||
extension::InsertTextFormat::PlainText => Self::PlainText,
|
||||
extension::InsertTextFormat::Snippet => Self::Snippet,
|
||||
extension::InsertTextFormat::Other(value) => Self::Other(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<extension::Symbol> for Symbol {
|
||||
fn from(value: extension::Symbol) -> Self {
|
||||
Self {
|
||||
kind: value.kind.into(),
|
||||
name: value.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<extension::SymbolKind> for SymbolKind {
|
||||
fn from(value: extension::SymbolKind) -> Self {
|
||||
match value {
|
||||
extension::SymbolKind::File => Self::File,
|
||||
extension::SymbolKind::Module => Self::Module,
|
||||
extension::SymbolKind::Namespace => Self::Namespace,
|
||||
extension::SymbolKind::Package => Self::Package,
|
||||
extension::SymbolKind::Class => Self::Class,
|
||||
extension::SymbolKind::Method => Self::Method,
|
||||
extension::SymbolKind::Property => Self::Property,
|
||||
extension::SymbolKind::Field => Self::Field,
|
||||
extension::SymbolKind::Constructor => Self::Constructor,
|
||||
extension::SymbolKind::Enum => Self::Enum,
|
||||
extension::SymbolKind::Interface => Self::Interface,
|
||||
extension::SymbolKind::Function => Self::Function,
|
||||
extension::SymbolKind::Variable => Self::Variable,
|
||||
extension::SymbolKind::Constant => Self::Constant,
|
||||
extension::SymbolKind::String => Self::String,
|
||||
extension::SymbolKind::Number => Self::Number,
|
||||
extension::SymbolKind::Boolean => Self::Boolean,
|
||||
extension::SymbolKind::Array => Self::Array,
|
||||
extension::SymbolKind::Object => Self::Object,
|
||||
extension::SymbolKind::Key => Self::Key,
|
||||
extension::SymbolKind::Null => Self::Null,
|
||||
extension::SymbolKind::EnumMember => Self::EnumMember,
|
||||
extension::SymbolKind::Struct => Self::Struct,
|
||||
extension::SymbolKind::Event => Self::Event,
|
||||
extension::SymbolKind::Operator => Self::Operator,
|
||||
extension::SymbolKind::TypeParameter => Self::TypeParameter,
|
||||
extension::SymbolKind::Other(value) => Self::Other(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<extension::SlashCommand> for SlashCommand {
|
||||
fn from(value: extension::SlashCommand) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -11,9 +11,6 @@ workspace = true
|
||||
[lib]
|
||||
path = "src/extensions_ui.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
@@ -25,7 +22,6 @@ editor.workspace = true
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
@@ -50,21 +46,4 @@ wasmtime-wasi.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
extension_host = {workspace = true, features = ["test-support"] }
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
reqwest_client.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -3,9 +3,6 @@ mod extension_registration_hooks;
|
||||
mod extension_suggest;
|
||||
mod extension_version_selector;
|
||||
|
||||
#[cfg(test)]
|
||||
mod extension_store_test;
|
||||
|
||||
pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks;
|
||||
|
||||
use std::ops::DerefMut;
|
||||
|
||||
@@ -10,11 +10,11 @@ pub use open_path_prompt::OpenPathDelegate;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||
use file_finder_settings::FileFinderSettings;
|
||||
use file_finder_settings::{FileFinderSettings, FileFinderWidth};
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
use gpui::{
|
||||
actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
||||
actions, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
||||
FocusableView, KeyContext, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render,
|
||||
Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
@@ -244,6 +244,22 @@ impl FileFinder {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn modal_max_width(
|
||||
width_setting: Option<FileFinderWidth>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Pixels {
|
||||
let window_width = cx.viewport_size().width;
|
||||
let small_width = Pixels(545.);
|
||||
|
||||
match width_setting {
|
||||
None | Some(FileFinderWidth::Small) => small_width,
|
||||
Some(FileFinderWidth::Full) => window_width,
|
||||
Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width),
|
||||
Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width),
|
||||
Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for FileFinder {}
|
||||
@@ -257,9 +273,13 @@ impl FocusableView for FileFinder {
|
||||
impl Render for FileFinder {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let key_context = self.picker.read(cx).delegate.key_context(cx);
|
||||
|
||||
let file_finder_settings = FileFinderSettings::get_global(cx);
|
||||
let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, cx);
|
||||
|
||||
v_flex()
|
||||
.key_context(key_context)
|
||||
.w(rems(34.))
|
||||
.w(modal_max_width)
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.on_action(cx.listener(Self::handle_select_prev))
|
||||
.on_action(cx.listener(Self::handle_open_menu))
|
||||
|
||||
@@ -6,6 +6,7 @@ use settings::{Settings, SettingsSources};
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct FileFinderSettings {
|
||||
pub file_icons: bool,
|
||||
pub modal_max_width: Option<FileFinderWidth>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -14,6 +15,10 @@ pub struct FileFinderSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub file_icons: Option<bool>,
|
||||
/// Determines how much space the file finder can take up in relation to the available window width.
|
||||
///
|
||||
/// Default: small
|
||||
pub modal_max_width: Option<FileFinderWidth>,
|
||||
}
|
||||
|
||||
impl Settings for FileFinderSettings {
|
||||
@@ -25,3 +30,14 @@ impl Settings for FileFinderSettings {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FileFinderWidth {
|
||||
#[default]
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
XLarge,
|
||||
Full,
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ mod supported_countries;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use supported_countries::*;
|
||||
|
||||
@@ -15,7 +14,6 @@ pub async fn stream_generate_content(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
mut request: GenerateContentRequest,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<BoxStream<'static, Result<GenerateContentResponse>>> {
|
||||
let uri = format!(
|
||||
"{api_url}/v1beta/models/{model}:streamGenerateContent?alt=sse&key={api_key}",
|
||||
@@ -23,15 +21,11 @@ pub async fn stream_generate_content(
|
||||
);
|
||||
request.model.clear();
|
||||
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if let Some(low_speed_timeout) = low_speed_timeout {
|
||||
request_builder = request_builder.read_timeout(low_speed_timeout);
|
||||
};
|
||||
|
||||
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
|
||||
let mut response = client.send(request).await?;
|
||||
if response.status().is_success() {
|
||||
@@ -70,7 +64,6 @@ pub async fn count_tokens(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: CountTokensRequest,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<CountTokensResponse> {
|
||||
let uri = format!(
|
||||
"{}/v1beta/models/gemini-pro:countTokens?key={}",
|
||||
@@ -78,15 +71,11 @@ pub async fn count_tokens(
|
||||
);
|
||||
let request = serde_json::to_string(&request)?;
|
||||
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(&uri)
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if let Some(low_speed_timeout) = low_speed_timeout {
|
||||
request_builder = request_builder.read_timeout(low_speed_timeout);
|
||||
}
|
||||
|
||||
let http_request = request_builder.body(AsyncBody::from(request))?;
|
||||
let mut response = client.send(http_request).await?;
|
||||
let mut text = String::new();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use gpui::{
|
||||
div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions,
|
||||
};
|
||||
use gpui::{div, img, prelude::*, App, AppContext, Render, ViewContext, WindowOptions};
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct GifViewer {
|
||||
@@ -16,7 +14,7 @@ impl GifViewer {
|
||||
impl Render for GifViewer {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().size_full().child(
|
||||
img(ImageSource::File(self.gif_path.clone().into()))
|
||||
img(self.gif_path.clone())
|
||||
.size_full()
|
||||
.object_fit(gpui::ObjectFit::Contain)
|
||||
.id("gif"),
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<circle cx="240" cy="100" r="30" stroke="#dc2626" />
|
||||
<circle cx="380" cy="100" r="20" stroke="#d97706" />
|
||||
<circle cx="380" cy="240" r="30" stroke="#06b6d4" />
|
||||
<circle cx="100" cy="240" r="30" stroke="#3b82f6" />
|
||||
<circle cx="100" cy="240" r="30" stroke="#3b82f666" />
|
||||
<circle cx="240" cy="380" r="30" stroke="#7c3aed" />
|
||||
<circle cx="380" cy="380" r="20" stroke="#c026d3" />
|
||||
<circle cx="100" cy="380" r="20" stroke="#e11d48" />
|
||||
</svg>
|
||||
<circle cx="100" cy="380" r="20" stroke="#e11d4866" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 741 B After Width: | Height: | Size: 746 B |
@@ -61,7 +61,7 @@ impl RenderOnce for ImageContainer {
|
||||
}
|
||||
|
||||
struct ImageShowcase {
|
||||
local_resource: Arc<PathBuf>,
|
||||
local_resource: Arc<std::path::Path>,
|
||||
remote_resource: SharedUri,
|
||||
asset_resource: SharedString,
|
||||
}
|
||||
@@ -153,9 +153,10 @@ fn main() {
|
||||
cx.open_window(window_options, |cx| {
|
||||
cx.new_view(|_cx| ImageShowcase {
|
||||
// Relative path to your root project path
|
||||
local_resource: Arc::new(
|
||||
PathBuf::from_str("crates/gpui/examples/image/app-icon.png").unwrap(),
|
||||
),
|
||||
local_resource: PathBuf::from_str("crates/gpui/examples/image/app-icon.png")
|
||||
.unwrap()
|
||||
.into(),
|
||||
|
||||
remote_resource: "https://picsum.photos/512/512".into(),
|
||||
|
||||
asset_resource: "image/color.svg".into(),
|
||||
|
||||
214
crates/gpui/examples/image_loading.rs
Normal file
214
crates/gpui/examples/image_loading.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gpui::{
|
||||
black, div, img, prelude::*, pulsating_between, px, red, size, Animation, AnimationExt, App,
|
||||
AppContext, Asset, AssetLogger, AssetSource, Bounds, Hsla, ImageAssetLoader, ImageCacheError,
|
||||
ImgResourceLoader, Length, Pixels, RenderImage, Resource, SharedString, ViewContext,
|
||||
WindowBounds, WindowContext, WindowOptions, LOADING_DELAY,
|
||||
};
|
||||
|
||||
struct Assets {}
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> anyhow::Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
std::fs::read(path)
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> anyhow::Result<Vec<SharedString>> {
|
||||
Ok(std::fs::read_dir(path)?
|
||||
.filter_map(|entry| {
|
||||
Some(SharedString::from(
|
||||
entry.ok()?.path().to_string_lossy().to_string(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
const IMAGE: &str = "examples/image/app-icon.png";
|
||||
|
||||
#[derive(Copy, Clone, Hash)]
|
||||
struct LoadImageParameters {
|
||||
timeout: Duration,
|
||||
fail: bool,
|
||||
}
|
||||
|
||||
struct LoadImageWithParameters {}
|
||||
|
||||
impl Asset for LoadImageWithParameters {
|
||||
type Source = LoadImageParameters;
|
||||
|
||||
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
||||
|
||||
fn load(
|
||||
parameters: Self::Source,
|
||||
cx: &mut AppContext,
|
||||
) -> impl std::future::Future<Output = Self::Output> + Send + 'static {
|
||||
let timer = cx.background_executor().timer(parameters.timeout);
|
||||
let data = AssetLogger::<ImageAssetLoader>::load(
|
||||
Resource::Path(Path::new(IMAGE).to_path_buf().into()),
|
||||
cx,
|
||||
);
|
||||
async move {
|
||||
timer.await;
|
||||
if parameters.fail {
|
||||
log::error!("Intentionally failed to load image");
|
||||
Err(anyhow!("Failed to load image").into())
|
||||
} else {
|
||||
data.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageLoadingExample {}
|
||||
|
||||
impl ImageLoadingExample {
|
||||
fn loading_element() -> impl IntoElement {
|
||||
div().size_full().flex_none().p_0p5().rounded_sm().child(
|
||||
div().size_full().with_animation(
|
||||
"loading-bg",
|
||||
Animation::new(Duration::from_secs(3))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.04, 0.24)),
|
||||
move |this, delta| this.bg(black().opacity(delta)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn fallback_element() -> impl IntoElement {
|
||||
let fallback_color: Hsla = black().opacity(0.5);
|
||||
|
||||
div().size_full().flex_none().p_0p5().child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_sm()
|
||||
.text_sm()
|
||||
.text_color(fallback_color)
|
||||
.border_1()
|
||||
.border_color(fallback_color)
|
||||
.child("?"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ImageLoadingExample {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().flex().flex_col().size_full().justify_around().child(
|
||||
div().flex().flex_row().w_full().justify_around().child(
|
||||
div()
|
||||
.flex()
|
||||
.bg(gpui::white())
|
||||
.size(Length::Definite(Pixels(300.0).into()))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child({
|
||||
let image_source = LoadImageParameters {
|
||||
timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)),
|
||||
fail: false,
|
||||
};
|
||||
|
||||
// Load within the 'loading delay', should not show loading fallback
|
||||
img(move |cx: &mut WindowContext| {
|
||||
cx.use_asset::<LoadImageWithParameters>(&image_source)
|
||||
})
|
||||
.id("image-1")
|
||||
.border_1()
|
||||
.size_12()
|
||||
.with_fallback(|| Self::fallback_element().into_any_element())
|
||||
.border_color(red())
|
||||
.with_loading(|| Self::loading_element().into_any_element())
|
||||
.on_click(move |_, cx| {
|
||||
cx.remove_asset::<LoadImageWithParameters>(&image_source);
|
||||
})
|
||||
})
|
||||
.child({
|
||||
// Load after a long delay
|
||||
let image_source = LoadImageParameters {
|
||||
timeout: Duration::from_secs(5),
|
||||
fail: false,
|
||||
};
|
||||
|
||||
img(move |cx: &mut WindowContext| {
|
||||
cx.use_asset::<LoadImageWithParameters>(&image_source)
|
||||
})
|
||||
.id("image-2")
|
||||
.with_fallback(|| Self::fallback_element().into_any_element())
|
||||
.with_loading(|| Self::loading_element().into_any_element())
|
||||
.size_12()
|
||||
.border_1()
|
||||
.border_color(red())
|
||||
.on_click(move |_, cx| {
|
||||
cx.remove_asset::<LoadImageWithParameters>(&image_source);
|
||||
})
|
||||
})
|
||||
.child({
|
||||
// Fail to load image after a long delay
|
||||
let image_source = LoadImageParameters {
|
||||
timeout: Duration::from_secs(5),
|
||||
fail: true,
|
||||
};
|
||||
|
||||
// Fail to load after a long delay
|
||||
img(move |cx: &mut WindowContext| {
|
||||
cx.use_asset::<LoadImageWithParameters>(&image_source)
|
||||
})
|
||||
.id("image-3")
|
||||
.with_fallback(|| Self::fallback_element().into_any_element())
|
||||
.with_loading(|| Self::loading_element().into_any_element())
|
||||
.size_12()
|
||||
.border_1()
|
||||
.border_color(red())
|
||||
.on_click(move |_, cx| {
|
||||
cx.remove_asset::<LoadImageWithParameters>(&image_source);
|
||||
})
|
||||
})
|
||||
.child({
|
||||
// Ensure that the normal image loader doesn't spam logs
|
||||
let image_source = Path::new(
|
||||
"this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope",
|
||||
)
|
||||
.to_path_buf();
|
||||
img(image_source.clone())
|
||||
.id("image-1")
|
||||
.border_1()
|
||||
.size_12()
|
||||
.with_fallback(|| Self::fallback_element().into_any_element())
|
||||
.border_color(red())
|
||||
.with_loading(|| Self::loading_element().into_any_element())
|
||||
.on_click(move |_, cx| {
|
||||
cx.remove_asset::<ImgResourceLoader>(&image_source.clone().into());
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
App::new()
|
||||
.with_assets(Assets {})
|
||||
.run(|cx: &mut AppContext| {
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(300.), Pixels(300.)),
|
||||
cx,
|
||||
))),
|
||||
..Default::default()
|
||||
};
|
||||
cx.open_window(options, |cx| {
|
||||
cx.activate(false);
|
||||
cx.new_view(|_cx| ImageLoadingExample {})
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
199
crates/gpui/examples/painting.rs
Normal file
199
crates/gpui/examples/painting.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use gpui::{
|
||||
canvas, div, point, prelude::*, px, size, App, AppContext, Bounds, MouseDownEvent, Path,
|
||||
Pixels, Point, Render, ViewContext, WindowOptions,
|
||||
};
|
||||
struct PaintingViewer {
|
||||
default_lines: Vec<Path<Pixels>>,
|
||||
lines: Vec<Vec<Point<Pixels>>>,
|
||||
start: Point<Pixels>,
|
||||
_painting: bool,
|
||||
}
|
||||
|
||||
impl PaintingViewer {
|
||||
fn new() -> Self {
|
||||
let mut lines = vec![];
|
||||
|
||||
// draw a line
|
||||
let mut path = Path::new(point(px(50.), px(180.)));
|
||||
path.line_to(point(px(100.), px(120.)));
|
||||
// go back to close the path
|
||||
path.line_to(point(px(100.), px(121.)));
|
||||
path.line_to(point(px(50.), px(181.)));
|
||||
lines.push(path);
|
||||
|
||||
// draw a lightening bolt ⚡
|
||||
let mut path = Path::new(point(px(150.), px(200.)));
|
||||
path.line_to(point(px(200.), px(125.)));
|
||||
path.line_to(point(px(200.), px(175.)));
|
||||
path.line_to(point(px(250.), px(100.)));
|
||||
lines.push(path);
|
||||
|
||||
// draw a ⭐
|
||||
let mut path = Path::new(point(px(350.), px(100.)));
|
||||
path.line_to(point(px(370.), px(160.)));
|
||||
path.line_to(point(px(430.), px(160.)));
|
||||
path.line_to(point(px(380.), px(200.)));
|
||||
path.line_to(point(px(400.), px(260.)));
|
||||
path.line_to(point(px(350.), px(220.)));
|
||||
path.line_to(point(px(300.), px(260.)));
|
||||
path.line_to(point(px(320.), px(200.)));
|
||||
path.line_to(point(px(270.), px(160.)));
|
||||
path.line_to(point(px(330.), px(160.)));
|
||||
path.line_to(point(px(350.), px(100.)));
|
||||
lines.push(path);
|
||||
|
||||
let square_bounds = Bounds {
|
||||
origin: point(px(450.), px(100.)),
|
||||
size: size(px(200.), px(80.)),
|
||||
};
|
||||
let height = square_bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(30.);
|
||||
let mut path = Path::new(square_bounds.lower_left());
|
||||
path.curve_to(
|
||||
square_bounds.origin + point(horizontal_offset, vertical_offset),
|
||||
square_bounds.origin + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(square_bounds.upper_right() + point(-horizontal_offset, vertical_offset));
|
||||
path.curve_to(
|
||||
square_bounds.lower_right(),
|
||||
square_bounds.upper_right() + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(square_bounds.lower_left());
|
||||
lines.push(path);
|
||||
|
||||
Self {
|
||||
default_lines: lines.clone(),
|
||||
lines: vec![],
|
||||
start: point(px(0.), px(0.)),
|
||||
_painting: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.lines.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
impl Render for PaintingViewer {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let default_lines = self.default_lines.clone();
|
||||
let lines = self.lines.clone();
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
|
||||
.child(
|
||||
div()
|
||||
.id("clear")
|
||||
.child("Clean up")
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.active(|this| this.opacity(0.8))
|
||||
.flex()
|
||||
.px_3()
|
||||
.py_1()
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.clear(cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.child(
|
||||
canvas(
|
||||
move |_, _| {},
|
||||
move |_, _, cx| {
|
||||
const STROKE_WIDTH: Pixels = px(2.0);
|
||||
for path in default_lines {
|
||||
cx.paint_path(path, gpui::black());
|
||||
}
|
||||
for points in lines {
|
||||
let mut path = Path::new(points[0]);
|
||||
for p in points.iter().skip(1) {
|
||||
path.line_to(*p);
|
||||
}
|
||||
|
||||
let mut last = points.last().unwrap();
|
||||
for p in points.iter().rev() {
|
||||
let mut offset_x = px(0.);
|
||||
if last.x == p.x {
|
||||
offset_x = STROKE_WIDTH;
|
||||
}
|
||||
path.line_to(point(p.x + offset_x, p.y + STROKE_WIDTH));
|
||||
last = p;
|
||||
}
|
||||
|
||||
cx.paint_path(path, gpui::black());
|
||||
}
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
.on_mouse_down(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(|this, ev: &MouseDownEvent, _| {
|
||||
this._painting = true;
|
||||
this.start = ev.position;
|
||||
let path = vec![ev.position];
|
||||
this.lines.push(path);
|
||||
}),
|
||||
)
|
||||
.on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, cx| {
|
||||
if !this._painting {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_shifted = ev.modifiers.shift;
|
||||
let mut pos = ev.position;
|
||||
// When holding shift, draw a straight line
|
||||
if is_shifted {
|
||||
let dx = pos.x - this.start.x;
|
||||
let dy = pos.y - this.start.y;
|
||||
if dx.abs() > dy.abs() {
|
||||
pos.y = this.start.y;
|
||||
} else {
|
||||
pos.x = this.start.x;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = this.lines.last_mut() {
|
||||
path.push(pos);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.on_mouse_up(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(|this, _, _| {
|
||||
this._painting = false;
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|_| PaintingViewer::new()),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -740,7 +740,7 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Returns the SVG renderer GPUI uses
|
||||
pub(crate) fn svg_renderer(&self) -> SvgRenderer {
|
||||
pub fn svg_renderer(&self) -> SvgRenderer {
|
||||
self.svg_renderer.clone()
|
||||
}
|
||||
|
||||
@@ -1362,7 +1362,7 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Remove an asset from GPUI's cache
|
||||
pub fn remove_cached_asset<A: Asset + 'static>(&mut self, source: &A::Source) {
|
||||
pub fn remove_asset<A: Asset>(&mut self, source: &A::Source) {
|
||||
let asset_id = (TypeId::of::<A>(), hash(source));
|
||||
self.loading_assets.remove(&asset_id);
|
||||
}
|
||||
@@ -1371,12 +1371,7 @@ impl AppContext {
|
||||
///
|
||||
/// Note that the multiple calls to this method will only result in one `Asset::load` call at a
|
||||
/// time, and the results of this call will be cached
|
||||
///
|
||||
/// This asset will not be cached by default, see [Self::use_cached_asset]
|
||||
pub fn fetch_asset<A: Asset + 'static>(
|
||||
&mut self,
|
||||
source: &A::Source,
|
||||
) -> (Shared<Task<A::Output>>, bool) {
|
||||
pub fn fetch_asset<A: Asset>(&mut self, source: &A::Source) -> (Shared<Task<A::Output>>, bool) {
|
||||
let asset_id = (TypeId::of::<A>(), hash(source));
|
||||
let mut is_first = false;
|
||||
let task = self
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
use crate::{AppContext, SharedString, SharedUri};
|
||||
use futures::Future;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// An enum representing
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub(crate) enum UriOrPath {
|
||||
pub enum Resource {
|
||||
/// This resource is at a given URI
|
||||
Uri(SharedUri),
|
||||
Path(Arc<PathBuf>),
|
||||
/// This resource is at a given path in the file system
|
||||
Path(Arc<Path>),
|
||||
/// This resource is embedded in the application binary
|
||||
Embedded(SharedString),
|
||||
}
|
||||
|
||||
impl From<SharedUri> for UriOrPath {
|
||||
impl From<SharedUri> for Resource {
|
||||
fn from(value: SharedUri) -> Self {
|
||||
Self::Uri(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<PathBuf>> for UriOrPath {
|
||||
fn from(value: Arc<PathBuf>) -> Self {
|
||||
impl From<PathBuf> for Resource {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
Self::Path(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Path>> for Resource {
|
||||
fn from(value: Arc<Path>) -> Self {
|
||||
Self::Path(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for asynchronous asset loading.
|
||||
pub trait Asset {
|
||||
pub trait Asset: 'static {
|
||||
/// The source of the asset.
|
||||
type Source: Clone + Hash + Send;
|
||||
|
||||
@@ -38,6 +51,31 @@ pub trait Asset {
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static;
|
||||
}
|
||||
|
||||
/// An asset Loader that logs whatever passes through it
|
||||
pub enum AssetLogger<T> {
|
||||
#[doc(hidden)]
|
||||
_Phantom(PhantomData<T>, &'static dyn crate::seal::Sealed),
|
||||
}
|
||||
|
||||
impl<R: Clone + Send, E: Clone + Send + std::error::Error, T: Asset<Output = Result<R, E>>> Asset
|
||||
for AssetLogger<T>
|
||||
{
|
||||
type Source = T::Source;
|
||||
|
||||
type Output = T::Output;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut AppContext,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let load = T::load(source, cx);
|
||||
async {
|
||||
load.await
|
||||
.inspect_err(|e| log::error!("Failed to load asset: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use a quick, non-cryptographically secure hash function to get an identifier from data
|
||||
pub fn hash<T: Hash>(data: &T) -> u64 {
|
||||
let mut hasher = collections::FxHasher::default();
|
||||
|
||||
@@ -22,6 +22,17 @@ pub fn rgba(hex: u32) -> Rgba {
|
||||
Rgba { r, g, b, a }
|
||||
}
|
||||
|
||||
/// Swap from RGBA with premultiplied alpha to BGRA
|
||||
pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
|
||||
color.swap(0, 2);
|
||||
if color[3] > 0 {
|
||||
let a = color[3] as f32 / 255.;
|
||||
color[0] = (color[0] as f32 / a) as u8;
|
||||
color[1] = (color[1] as f32 / a) as u8;
|
||||
color[2] = (color[2] as f32 / a) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
/// An RGBA color
|
||||
#[derive(PartialEq, Clone, Copy, Default)]
|
||||
pub struct Rgba {
|
||||
|
||||
@@ -443,7 +443,7 @@ impl Interactivity {
|
||||
pub fn on_drag<T, W>(
|
||||
&mut self,
|
||||
value: T,
|
||||
constructor: impl Fn(&T, &mut WindowContext) -> View<W> + 'static,
|
||||
constructor: impl Fn(&T, Point<Pixels>, &mut WindowContext) -> View<W> + 'static,
|
||||
) where
|
||||
Self: Sized,
|
||||
T: 'static,
|
||||
@@ -455,7 +455,9 @@ impl Interactivity {
|
||||
);
|
||||
self.drag_listener = Some((
|
||||
Box::new(value),
|
||||
Box::new(move |value, cx| constructor(value.downcast_ref().unwrap(), cx).into()),
|
||||
Box::new(move |value, offset, cx| {
|
||||
constructor(value.downcast_ref().unwrap(), offset, cx).into()
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -966,14 +968,15 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
|
||||
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
|
||||
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
|
||||
/// the [`Self::on_drag_move`] API
|
||||
/// the [`Self::on_drag_move`] API.
|
||||
/// The callback also has access to the offset of triggering click from the origin of parent element.
|
||||
/// The fluent API equivalent to [`Interactivity::on_drag`]
|
||||
///
|
||||
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
|
||||
fn on_drag<T, W>(
|
||||
mut self,
|
||||
value: T,
|
||||
constructor: impl Fn(&T, &mut WindowContext) -> View<W> + 'static,
|
||||
constructor: impl Fn(&T, Point<Pixels>, &mut WindowContext) -> View<W> + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -1056,7 +1059,8 @@ pub(crate) type ScrollWheelListener =
|
||||
|
||||
pub(crate) type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
|
||||
|
||||
pub(crate) type DragListener = Box<dyn Fn(&dyn Any, &mut WindowContext) -> AnyView + 'static>;
|
||||
pub(crate) type DragListener =
|
||||
Box<dyn Fn(&dyn Any, Point<Pixels>, &mut WindowContext) -> AnyView + 'static>;
|
||||
|
||||
type DropListener = Box<dyn Fn(&dyn Any, &mut WindowContext) + 'static>;
|
||||
|
||||
@@ -1818,7 +1822,8 @@ impl Interactivity {
|
||||
if let Some((drag_value, drag_listener)) = drag_listener.take() {
|
||||
*clicked_state.borrow_mut() = ElementClickedState::default();
|
||||
let cursor_offset = event.position - hitbox.origin;
|
||||
let drag = (drag_listener)(drag_value.as_ref(), cx);
|
||||
let drag =
|
||||
(drag_listener)(drag_value.as_ref(), cursor_offset, cx);
|
||||
cx.active_drag = Some(AnyDrag {
|
||||
view: drag,
|
||||
value: drag_value,
|
||||
@@ -2378,7 +2383,7 @@ where
|
||||
|
||||
/// A wrapper around an element that can store state, produced after assigning an ElementId.
|
||||
pub struct Stateful<E> {
|
||||
element: E,
|
||||
pub(crate) element: E,
|
||||
}
|
||||
|
||||
impl<E> Styled for Stateful<E>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::{
|
||||
px, AbsoluteLength, AppContext, Asset, Bounds, DefiniteLength, Element, ElementId,
|
||||
GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
||||
Length, ObjectFit, Pixels, RenderImage, SharedString, SharedUri, StyleRefinement, Styled,
|
||||
SvgSize, UriOrPath, WindowContext,
|
||||
px, swap_rgba_pa_to_bgra, AbsoluteLength, AnyElement, AppContext, Asset, AssetLogger, Bounds,
|
||||
DefiniteLength, Element, ElementId, GlobalElementId, Hitbox, Image, InteractiveElement,
|
||||
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
|
||||
SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task, WindowContext,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use futures::{AsyncReadExt, Future};
|
||||
use image::{
|
||||
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
|
||||
@@ -11,45 +13,56 @@ use image::{
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::PathBuf,
|
||||
io::{self, Cursor},
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
use super::{FocusableElement, Stateful, StatefulInteractiveElement};
|
||||
|
||||
/// The delay before showing the loading state.
|
||||
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
|
||||
|
||||
/// A type alias to the resource loader that the `img()` element uses.
|
||||
///
|
||||
/// Note: that this is only for Resources, like URLs or file paths.
|
||||
/// Custom loaders, or external images will not use this asset loader
|
||||
pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
|
||||
|
||||
/// A source of image content.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone)]
|
||||
pub enum ImageSource {
|
||||
/// Image content will be loaded from provided URI at render time.
|
||||
Uri(SharedUri),
|
||||
/// Image content will be loaded from the provided file at render time.
|
||||
File(Arc<PathBuf>),
|
||||
/// The image content will be loaded from some resource location
|
||||
Resource(Resource),
|
||||
/// Cached image data
|
||||
Render(Arc<RenderImage>),
|
||||
/// Cached image data
|
||||
Image(Arc<Image>),
|
||||
/// Image content will be loaded from Asset at render time.
|
||||
Embedded(SharedString),
|
||||
/// A custom loading function to use
|
||||
Custom(Arc<dyn Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
|
||||
}
|
||||
|
||||
fn is_uri(uri: &str) -> bool {
|
||||
uri.contains("://")
|
||||
http_client::Uri::from_str(uri).is_ok()
|
||||
}
|
||||
|
||||
impl From<SharedUri> for ImageSource {
|
||||
fn from(value: SharedUri) -> Self {
|
||||
Self::Uri(value)
|
||||
Self::Resource(Resource::Uri(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for ImageSource {
|
||||
fn from(s: &'static str) -> Self {
|
||||
impl<'a> From<&'a str> for ImageSource {
|
||||
fn from(s: &'a str) -> Self {
|
||||
if is_uri(s) {
|
||||
Self::Uri(s.into())
|
||||
Self::Resource(Resource::Uri(s.to_string().into()))
|
||||
} else {
|
||||
Self::Embedded(s.into())
|
||||
Self::Resource(Resource::Embedded(s.to_string().into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,32 +70,34 @@ impl From<&'static str> for ImageSource {
|
||||
impl From<String> for ImageSource {
|
||||
fn from(s: String) -> Self {
|
||||
if is_uri(&s) {
|
||||
Self::Uri(s.into())
|
||||
Self::Resource(Resource::Uri(s.into()))
|
||||
} else {
|
||||
Self::Embedded(s.into())
|
||||
Self::Resource(Resource::Embedded(s.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for ImageSource {
|
||||
fn from(s: SharedString) -> Self {
|
||||
if is_uri(&s) {
|
||||
Self::Uri(s.into())
|
||||
} else {
|
||||
Self::Embedded(s)
|
||||
}
|
||||
s.as_ref().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<PathBuf>> for ImageSource {
|
||||
fn from(value: Arc<PathBuf>) -> Self {
|
||||
Self::File(value)
|
||||
impl From<&Path> for ImageSource {
|
||||
fn from(value: &Path) -> Self {
|
||||
Self::Resource(value.to_path_buf().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Path>> for ImageSource {
|
||||
fn from(value: Arc<Path>) -> Self {
|
||||
Self::Resource(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for ImageSource {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
Self::File(value.into())
|
||||
Self::Resource(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,12 +113,80 @@ impl From<Arc<Image>> for ImageSource {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static>
|
||||
From<F> for ImageSource
|
||||
{
|
||||
fn from(value: F) -> Self {
|
||||
Self::Custom(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// The style of an image element.
|
||||
pub struct ImageStyle {
|
||||
grayscale: bool,
|
||||
object_fit: ObjectFit,
|
||||
loading: Option<Box<dyn Fn() -> AnyElement>>,
|
||||
fallback: Option<Box<dyn Fn() -> AnyElement>>,
|
||||
}
|
||||
|
||||
impl Default for ImageStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
grayscale: false,
|
||||
object_fit: ObjectFit::Contain,
|
||||
loading: None,
|
||||
fallback: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Style an image element.
|
||||
pub trait StyledImage: Sized {
|
||||
/// Get a mutable [ImageStyle] from the element.
|
||||
fn image_style(&mut self) -> &mut ImageStyle;
|
||||
|
||||
/// Set the image to be displayed in grayscale.
|
||||
fn grayscale(mut self, grayscale: bool) -> Self {
|
||||
self.image_style().grayscale = grayscale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the object fit for the image.
|
||||
fn object_fit(mut self, object_fit: ObjectFit) -> Self {
|
||||
self.image_style().object_fit = object_fit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the object fit for the image.
|
||||
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
|
||||
self.image_style().fallback = Some(Box::new(fallback));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the object fit for the image.
|
||||
fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
|
||||
self.image_style().loading = Some(Box::new(loading));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl StyledImage for Img {
|
||||
fn image_style(&mut self) -> &mut ImageStyle {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl StyledImage for Stateful<Img> {
|
||||
fn image_style(&mut self) -> &mut ImageStyle {
|
||||
&mut self.element.style
|
||||
}
|
||||
}
|
||||
|
||||
/// An image element.
|
||||
pub struct Img {
|
||||
interactivity: Interactivity,
|
||||
source: ImageSource,
|
||||
grayscale: bool,
|
||||
object_fit: ObjectFit,
|
||||
style: ImageStyle,
|
||||
}
|
||||
|
||||
/// Create a new image element.
|
||||
@@ -111,8 +194,7 @@ pub fn img(source: impl Into<ImageSource>) -> Img {
|
||||
Img {
|
||||
interactivity: Interactivity::default(),
|
||||
source: source.into(),
|
||||
grayscale: false,
|
||||
object_fit: ObjectFit::Contain,
|
||||
style: ImageStyle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +207,19 @@ impl Img {
|
||||
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the image to be displayed in grayscale.
|
||||
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
||||
self.grayscale = grayscale;
|
||||
self
|
||||
impl Deref for Stateful<Img> {
|
||||
type Target = Img;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.element
|
||||
}
|
||||
/// Set the object fit for the image.
|
||||
pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
|
||||
self.object_fit = object_fit;
|
||||
self
|
||||
}
|
||||
|
||||
impl DerefMut for Stateful<Img> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.element
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,10 +227,17 @@ impl Img {
|
||||
struct ImgState {
|
||||
frame_index: usize,
|
||||
last_frame_time: Option<Instant>,
|
||||
started_loading: Option<(Instant, Task<()>)>,
|
||||
}
|
||||
|
||||
/// The image layout state between frames
|
||||
pub struct ImgLayoutState {
|
||||
frame_index: usize,
|
||||
replacement: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl Element for Img {
|
||||
type RequestLayoutState = usize;
|
||||
type RequestLayoutState = ImgLayoutState;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -157,11 +249,17 @@ impl Element for Img {
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut layout_state = ImgLayoutState {
|
||||
frame_index: 0,
|
||||
replacement: None,
|
||||
};
|
||||
|
||||
cx.with_optional_element_state(global_id, |state, cx| {
|
||||
let mut state = state.map(|state| {
|
||||
state.unwrap_or(ImgState {
|
||||
frame_index: 0,
|
||||
last_frame_time: None,
|
||||
started_loading: None,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -170,64 +268,105 @@ impl Element for Img {
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.use_data(cx) {
|
||||
if let Some(state) = &mut state {
|
||||
let frame_count = data.frame_count();
|
||||
if frame_count > 1 {
|
||||
let current_time = Instant::now();
|
||||
if let Some(last_frame_time) = state.last_frame_time {
|
||||
let elapsed = current_time - last_frame_time;
|
||||
let frame_duration =
|
||||
Duration::from(data.delay(state.frame_index));
|
||||
let mut replacement_id = None;
|
||||
|
||||
if elapsed >= frame_duration {
|
||||
state.frame_index = (state.frame_index + 1) % frame_count;
|
||||
state.last_frame_time =
|
||||
Some(current_time - (elapsed - frame_duration));
|
||||
match self.source.use_data(cx) {
|
||||
Some(Ok(data)) => {
|
||||
if let Some(state) = &mut state {
|
||||
let frame_count = data.frame_count();
|
||||
if frame_count > 1 {
|
||||
let current_time = Instant::now();
|
||||
if let Some(last_frame_time) = state.last_frame_time {
|
||||
let elapsed = current_time - last_frame_time;
|
||||
let frame_duration =
|
||||
Duration::from(data.delay(state.frame_index));
|
||||
|
||||
if elapsed >= frame_duration {
|
||||
state.frame_index =
|
||||
(state.frame_index + 1) % frame_count;
|
||||
state.last_frame_time =
|
||||
Some(current_time - (elapsed - frame_duration));
|
||||
}
|
||||
} else {
|
||||
state.last_frame_time = Some(current_time);
|
||||
}
|
||||
}
|
||||
state.started_loading = None;
|
||||
}
|
||||
|
||||
let image_size = data.size(frame_index);
|
||||
|
||||
if let Length::Auto = style.size.width {
|
||||
style.size.width = match style.size.height {
|
||||
Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(height),
|
||||
)) => Length::Definite(
|
||||
px(image_size.width.0 as f32 * height.0
|
||||
/ image_size.height.0 as f32)
|
||||
.into(),
|
||||
),
|
||||
_ => Length::Definite(px(image_size.width.0 as f32).into()),
|
||||
};
|
||||
}
|
||||
|
||||
if let Length::Auto = style.size.height {
|
||||
style.size.height = match style.size.width {
|
||||
Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(width),
|
||||
)) => Length::Definite(
|
||||
px(image_size.height.0 as f32 * width.0
|
||||
/ image_size.width.0 as f32)
|
||||
.into(),
|
||||
),
|
||||
_ => Length::Definite(px(image_size.height.0 as f32).into()),
|
||||
};
|
||||
}
|
||||
|
||||
if global_id.is_some() && data.frame_count() > 1 {
|
||||
cx.request_animation_frame();
|
||||
}
|
||||
}
|
||||
Some(_err) => {
|
||||
if let Some(fallback) = self.style.fallback.as_ref() {
|
||||
let mut element = fallback();
|
||||
replacement_id = Some(element.request_layout(cx));
|
||||
layout_state.replacement = Some(element);
|
||||
}
|
||||
if let Some(state) = &mut state {
|
||||
state.started_loading = None;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(state) = &mut state {
|
||||
if let Some((started_loading, _)) = state.started_loading {
|
||||
if started_loading.elapsed() > LOADING_DELAY {
|
||||
if let Some(loading) = self.style.loading.as_ref() {
|
||||
let mut element = loading();
|
||||
replacement_id = Some(element.request_layout(cx));
|
||||
layout_state.replacement = Some(element);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.last_frame_time = Some(current_time);
|
||||
let parent_view_id = cx.parent_view_id();
|
||||
let task = cx.spawn(|mut cx| async move {
|
||||
cx.background_executor().timer(LOADING_DELAY).await;
|
||||
cx.update(|cx| {
|
||||
cx.notify(parent_view_id);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
state.started_loading = Some((Instant::now(), task));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let image_size = data.size(frame_index);
|
||||
|
||||
if let Length::Auto = style.size.width {
|
||||
style.size.width = match style.size.height {
|
||||
Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(height),
|
||||
)) => Length::Definite(
|
||||
px(image_size.width.0 as f32 * height.0
|
||||
/ image_size.height.0 as f32)
|
||||
.into(),
|
||||
),
|
||||
_ => Length::Definite(px(image_size.width.0 as f32).into()),
|
||||
};
|
||||
}
|
||||
|
||||
if let Length::Auto = style.size.height {
|
||||
style.size.height = match style.size.width {
|
||||
Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(width),
|
||||
)) => Length::Definite(
|
||||
px(image_size.height.0 as f32 * width.0
|
||||
/ image_size.width.0 as f32)
|
||||
.into(),
|
||||
),
|
||||
_ => Length::Definite(px(image_size.height.0 as f32).into()),
|
||||
};
|
||||
}
|
||||
|
||||
if global_id.is_some() && data.frame_count() > 1 {
|
||||
cx.request_animation_frame();
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(style, [])
|
||||
cx.request_layout(style, replacement_id)
|
||||
});
|
||||
|
||||
((layout_id, frame_index), state)
|
||||
layout_state.frame_index = frame_index;
|
||||
|
||||
((layout_id, layout_state), state)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -235,18 +374,24 @@ impl Element for Img {
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Hitbox> {
|
||||
) -> Self::PrepaintState {
|
||||
self.interactivity
|
||||
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, cx| {
|
||||
if let Some(replacement) = &mut request_layout.replacement {
|
||||
replacement.prepaint(cx);
|
||||
}
|
||||
|
||||
hitbox
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
frame_index: &mut Self::RequestLayoutState,
|
||||
layout_state: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
@@ -255,29 +400,26 @@ impl Element for Img {
|
||||
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
|
||||
if let Some(data) = source.use_data(cx) {
|
||||
let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
|
||||
if let Some(Ok(data)) = source.use_data(cx) {
|
||||
let new_bounds = self
|
||||
.style
|
||||
.object_fit
|
||||
.get_bounds(bounds, data.size(layout_state.frame_index));
|
||||
cx.paint_image(
|
||||
new_bounds,
|
||||
corner_radii,
|
||||
data.clone(),
|
||||
*frame_index,
|
||||
self.grayscale,
|
||||
layout_state.frame_index,
|
||||
self.style.grayscale,
|
||||
)
|
||||
.log_err();
|
||||
} else if let Some(replacement) = &mut layout_state.replacement {
|
||||
replacement.paint(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Img {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Img {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
@@ -290,41 +432,28 @@ impl InteractiveElement for Img {
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageSource {
|
||||
pub(crate) fn use_data(&self, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
|
||||
match self {
|
||||
ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
|
||||
let uri_or_path: UriOrPath = match self {
|
||||
ImageSource::Uri(uri) => uri.clone().into(),
|
||||
ImageSource::File(path) => path.clone().into(),
|
||||
ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
impl IntoElement for Img {
|
||||
type Element = Self;
|
||||
|
||||
cx.use_asset::<ImageAsset>(&uri_or_path)?.log_err()
|
||||
}
|
||||
|
||||
ImageSource::Render(data) => Some(data.to_owned()),
|
||||
ImageSource::Image(data) => cx.use_asset::<ImageDecoder>(data)?.log_err(),
|
||||
}
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the data associated with this source, using GPUI's asset caching
|
||||
pub async fn data(&self, cx: &mut AppContext) -> Option<Arc<RenderImage>> {
|
||||
impl FocusableElement for Img {}
|
||||
|
||||
impl StatefulInteractiveElement for Img {}
|
||||
|
||||
impl ImageSource {
|
||||
pub(crate) fn use_data(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
match self {
|
||||
ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
|
||||
let uri_or_path: UriOrPath = match self {
|
||||
ImageSource::Uri(uri) => uri.clone().into(),
|
||||
ImageSource::File(path) => path.clone().into(),
|
||||
ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.fetch_asset::<ImageAsset>(&uri_or_path).0.await.log_err()
|
||||
}
|
||||
|
||||
ImageSource::Render(data) => Some(data.to_owned()),
|
||||
ImageSource::Image(data) => cx.fetch_asset::<ImageDecoder>(data).0.await.log_err(),
|
||||
ImageSource::Resource(resource) => cx.use_asset::<ImgResourceLoader>(&resource),
|
||||
ImageSource::Custom(loading_fn) => loading_fn(cx),
|
||||
ImageSource::Render(data) => Some(Ok(data.to_owned())),
|
||||
ImageSource::Image(data) => cx.use_asset::<AssetLogger<ImageDecoder>>(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,22 +463,23 @@ enum ImageDecoder {}
|
||||
|
||||
impl Asset for ImageDecoder {
|
||||
type Source = Arc<Image>;
|
||||
type Output = Result<Arc<RenderImage>, Arc<anyhow::Error>>;
|
||||
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut AppContext,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let result = source.to_image_data(cx).map_err(Arc::new);
|
||||
async { result }
|
||||
let renderer = cx.svg_renderer();
|
||||
async move { source.to_image_data(renderer).map_err(Into::into) }
|
||||
}
|
||||
}
|
||||
|
||||
/// An image loader for the GPUI asset system
|
||||
#[derive(Clone)]
|
||||
enum ImageAsset {}
|
||||
pub enum ImageAssetLoader {}
|
||||
|
||||
impl Asset for ImageAsset {
|
||||
type Source = UriOrPath;
|
||||
impl Asset for ImageAssetLoader {
|
||||
type Source = Resource;
|
||||
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
||||
|
||||
fn load(
|
||||
@@ -363,12 +493,12 @@ impl Asset for ImageAsset {
|
||||
let asset_source = cx.asset_source().clone();
|
||||
async move {
|
||||
let bytes = match source.clone() {
|
||||
UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
|
||||
UriOrPath::Uri(uri) => {
|
||||
Resource::Path(uri) => fs::read(uri.as_ref())?,
|
||||
Resource::Uri(uri) => {
|
||||
let mut response = client
|
||||
.get(uri.as_ref(), ().into(), true)
|
||||
.await
|
||||
.map_err(|e| ImageCacheError::Client(Arc::new(e)))?;
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
if !response.status().is_success() {
|
||||
@@ -383,13 +513,13 @@ impl Asset for ImageAsset {
|
||||
}
|
||||
body
|
||||
}
|
||||
UriOrPath::Embedded(path) => {
|
||||
Resource::Embedded(path) => {
|
||||
let data = asset_source.load(&path).ok().flatten();
|
||||
if let Some(data) = data {
|
||||
data.to_vec()
|
||||
} else {
|
||||
return Err(ImageCacheError::Asset(
|
||||
format!("not found: {}", path).into(),
|
||||
format!("Embedded resource not found: {}", path).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -434,9 +564,8 @@ impl Asset for ImageAsset {
|
||||
let mut buffer =
|
||||
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
|
||||
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in buffer.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
swap_rgba_pa_to_bgra(pixel);
|
||||
}
|
||||
|
||||
RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
|
||||
@@ -450,9 +579,9 @@ impl Asset for ImageAsset {
|
||||
/// An error that can occur when interacting with the image cache.
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum ImageCacheError {
|
||||
/// An error that occurred while fetching an image from a remote source.
|
||||
#[error("http error: {0}")]
|
||||
Client(#[from] Arc<anyhow::Error>),
|
||||
/// Some other kind of error occurred
|
||||
#[error("error: {0}")]
|
||||
Other(#[from] Arc<anyhow::Error>),
|
||||
/// An error that occurred while reading the image from disk.
|
||||
#[error("IO error: {0}")]
|
||||
Io(Arc<std::io::Error>),
|
||||
@@ -477,20 +606,26 @@ pub enum ImageCacheError {
|
||||
Usvg(Arc<usvg::Error>),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ImageCacheError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self::Io(Arc::new(error))
|
||||
impl From<anyhow::Error> for ImageCacheError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Self::Other(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ImageError> for ImageCacheError {
|
||||
fn from(error: ImageError) -> Self {
|
||||
Self::Image(Arc::new(error))
|
||||
impl From<io::Error> for ImageCacheError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usvg::Error> for ImageCacheError {
|
||||
fn from(error: usvg::Error) -> Self {
|
||||
Self::Usvg(Arc::new(error))
|
||||
fn from(value: usvg::Error) -> Self {
|
||||
Self::Usvg(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<image::ImageError> for ImageCacheError {
|
||||
fn from(value: image::ImageError) -> Self {
|
||||
Self::Image(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ mod test;
|
||||
mod windows;
|
||||
|
||||
use crate::{
|
||||
point, Action, AnyWindowHandle, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds,
|
||||
DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor,
|
||||
GPUSpecs, GlyphId, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point,
|
||||
RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
|
||||
SharedString, Size, SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
|
||||
point, Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||
DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GPUSpecs, GlyphId,
|
||||
ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage,
|
||||
RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size, SvgRenderer,
|
||||
SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_task::Runnable;
|
||||
@@ -1264,11 +1264,13 @@ impl Image {
|
||||
|
||||
/// Use the GPUI `use_asset` API to make this image renderable
|
||||
pub fn use_render_image(self: Arc<Self>, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
|
||||
ImageSource::Image(self).use_data(cx)
|
||||
ImageSource::Image(self)
|
||||
.use_data(cx)
|
||||
.and_then(|result| result.ok())
|
||||
}
|
||||
|
||||
/// Convert the clipboard image to an `ImageData` object.
|
||||
pub fn to_image_data(&self, cx: &AppContext) -> Result<Arc<RenderImage>> {
|
||||
pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
|
||||
fn frames_for_image(
|
||||
bytes: &[u8],
|
||||
format: image::ImageFormat,
|
||||
@@ -1305,10 +1307,7 @@ impl Image {
|
||||
ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
|
||||
ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
|
||||
ImageFormat::Svg => {
|
||||
// TODO: Fix this
|
||||
let pixmap = cx
|
||||
.svg_renderer()
|
||||
.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
|
||||
let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
|
||||
|
||||
let buffer =
|
||||
image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())
|
||||
|
||||
@@ -222,6 +222,8 @@ fn is_printable_key(key: &str) -> bool {
|
||||
| "insert"
|
||||
| "home"
|
||||
| "end"
|
||||
| "back"
|
||||
| "forward"
|
||||
| "escape"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -684,6 +684,8 @@ impl Keystroke {
|
||||
Keysym::ISO_Left_Tab => "tab".to_owned(),
|
||||
Keysym::KP_Prior => "pageup".to_owned(),
|
||||
Keysym::KP_Next => "pagedown".to_owned(),
|
||||
Keysym::XF86_Back => "back".to_owned(),
|
||||
Keysym::XF86_Forward => "forward".to_owned(),
|
||||
|
||||
Keysym::comma => ",".to_owned(),
|
||||
Keysym::period => ".".to_owned(),
|
||||
|
||||
@@ -260,7 +260,10 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
let key = match first_char {
|
||||
Some(SPACE_KEY) => "space".to_string(),
|
||||
Some(SPACE_KEY) => {
|
||||
ime_key = Some(" ".to_string());
|
||||
"space".to_string()
|
||||
}
|
||||
Some(BACKSPACE_KEY) => "backspace".to_string(),
|
||||
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
|
||||
Some(ESCAPE_KEY) => "escape".to_string(),
|
||||
|
||||
@@ -343,8 +343,10 @@ impl MacPlatform {
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
let _: () =
|
||||
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
|
||||
if MacPlatform::os_version().unwrap() >= SemanticVersion::new(12, 0, 0) {
|
||||
let _: () =
|
||||
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
|
||||
}
|
||||
item.setKeyEquivalentModifierMask_(mask);
|
||||
}
|
||||
// For multi-keystroke bindings, render the keystroke as part of the title.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
point, px, size, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics,
|
||||
FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
|
||||
RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS,
|
||||
point, px, size, swap_rgba_pa_to_bgra, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures,
|
||||
FontId, FontMetrics, FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels,
|
||||
PlatformTextSystem, Point, RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString,
|
||||
Size, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use cocoa::appkit::CGFloat;
|
||||
@@ -418,11 +419,7 @@ impl MacTextSystemState {
|
||||
if params.is_emoji {
|
||||
// Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
|
||||
for pixel in bytes.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
let a = pixel[3] as f32 / 255.;
|
||||
pixel[0] = (pixel[0] as f32 / a) as u8;
|
||||
pixel[1] = (pixel[1] as f32 / a) as u8;
|
||||
pixel[2] = (pixel[2] as f32 / a) as u8;
|
||||
swap_rgba_pa_to_bgra(pixel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1160,6 +1160,8 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
|
||||
VK_END => "end",
|
||||
VK_PRIOR => "pageup",
|
||||
VK_NEXT => "pagedown",
|
||||
VK_BROWSER_BACK => "back",
|
||||
VK_BROWSER_FORWARD => "forward",
|
||||
VK_ESCAPE => "escape",
|
||||
VK_INSERT => "insert",
|
||||
VK_DELETE => "delete",
|
||||
@@ -1196,6 +1198,8 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
|
||||
VK_END => "end",
|
||||
VK_PRIOR => "pageup",
|
||||
VK_NEXT => "pagedown",
|
||||
VK_BROWSER_BACK => "back",
|
||||
VK_BROWSER_FORWARD => "forward",
|
||||
VK_ESCAPE => "escape",
|
||||
VK_INSERT => "insert",
|
||||
VK_DELETE => "delete",
|
||||
|
||||
@@ -647,11 +647,47 @@ impl PlatformWindow for WindowsWindow {
|
||||
}
|
||||
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
self.0
|
||||
.state
|
||||
.borrow_mut()
|
||||
let mut window_state = self.0.state.borrow_mut();
|
||||
window_state
|
||||
.renderer
|
||||
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
|
||||
let mut version = unsafe { std::mem::zeroed() };
|
||||
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) };
|
||||
if status.is_ok() {
|
||||
if background_appearance == WindowBackgroundAppearance::Blurred {
|
||||
if version.dwBuildNumber >= 17763 {
|
||||
set_window_composition_attribute(window_state.hwnd, Some((0, 0, 0, 10)), 4);
|
||||
}
|
||||
} else {
|
||||
if version.dwBuildNumber >= 17763 {
|
||||
set_window_composition_attribute(window_state.hwnd, None, 0);
|
||||
}
|
||||
}
|
||||
//Transparent effect might cause some flickering and performance issues due `WS_EX_COMPOSITED` is enabled
|
||||
//if `WS_EX_COMPOSITED` is removed the window instance won't initiate
|
||||
if background_appearance == WindowBackgroundAppearance::Transparent {
|
||||
unsafe {
|
||||
let current_style = GetWindowLongW(window_state.hwnd, GWL_EXSTYLE);
|
||||
SetWindowLongW(
|
||||
window_state.hwnd,
|
||||
GWL_EXSTYLE,
|
||||
current_style | WS_EX_LAYERED.0 as i32 | WS_EX_COMPOSITED.0 as i32,
|
||||
);
|
||||
SetLayeredWindowAttributes(window_state.hwnd, COLORREF(0), 225, LWA_ALPHA)
|
||||
.inspect_err(|e| log::error!("Unable to set window to transparent: {e}"))
|
||||
.ok();
|
||||
};
|
||||
} else {
|
||||
unsafe {
|
||||
let current_style = GetWindowLongW(window_state.hwnd, GWL_EXSTYLE);
|
||||
SetWindowLongW(
|
||||
window_state.hwnd,
|
||||
GWL_EXSTYLE,
|
||||
current_style & !WS_EX_LAYERED.0 as i32 & !WS_EX_COMPOSITED.0 as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
@@ -932,6 +968,23 @@ struct StyleAndBounds {
|
||||
cy: i32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct WINDOWCOMPOSITIONATTRIBDATA {
|
||||
attrib: u32,
|
||||
pv_data: *mut std::ffi::c_void,
|
||||
cb_data: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct AccentPolicy {
|
||||
accent_state: u32,
|
||||
accent_flags: u32,
|
||||
gradient_color: u32,
|
||||
animation_id: u32,
|
||||
}
|
||||
|
||||
type Color = (u8, u8, u8, u8);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct WindowBorderOffset {
|
||||
width_offset: i32,
|
||||
@@ -1136,6 +1189,44 @@ fn retrieve_window_placement(
|
||||
Ok(placement)
|
||||
}
|
||||
|
||||
fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32) {
|
||||
unsafe {
|
||||
type SetWindowCompositionAttributeType =
|
||||
unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL;
|
||||
let module_name = PCSTR::from_raw("user32.dll\0".as_ptr());
|
||||
let user32 = GetModuleHandleA(module_name);
|
||||
if user32.is_ok() {
|
||||
let func_name = PCSTR::from_raw("SetWindowCompositionAttribute\0".as_ptr());
|
||||
let set_window_composition_attribute: SetWindowCompositionAttributeType =
|
||||
std::mem::transmute(GetProcAddress(user32.unwrap(), func_name));
|
||||
let mut color = color.unwrap_or_default();
|
||||
let is_acrylic = state == 4;
|
||||
if is_acrylic && color.3 == 0 {
|
||||
color.3 = 1;
|
||||
}
|
||||
let accent = AccentPolicy {
|
||||
accent_state: state,
|
||||
accent_flags: if is_acrylic { 0 } else { 2 },
|
||||
gradient_color: (color.0 as u32)
|
||||
| ((color.1 as u32) << 8)
|
||||
| ((color.2 as u32) << 16)
|
||||
| (color.3 as u32) << 24,
|
||||
animation_id: 0,
|
||||
};
|
||||
let mut data = WINDOWCOMPOSITIONATTRIBDATA {
|
||||
attrib: 0x13,
|
||||
pv_data: &accent as *const _ as *mut _,
|
||||
cb_data: std::mem::size_of::<AccentPolicy>(),
|
||||
};
|
||||
let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _);
|
||||
} else {
|
||||
let _ = user32
|
||||
.inspect_err(|e| log::error!("Error getting module: {e}"))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod windows_renderer {
|
||||
use std::{num::NonZeroIsize, sync::Arc};
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
pub use crate::{
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
|
||||
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, VisualContext,
|
||||
StatefulInteractiveElement, Styled, StyledImage, VisualContext,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength,
|
||||
Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length,
|
||||
SharedString, StyleRefinement, WhiteSpace,
|
||||
SharedString, StrikethroughStyle, StyleRefinement, WhiteSpace,
|
||||
};
|
||||
use crate::{TextStyleRefinement, Truncate};
|
||||
pub use gpui_macros::{
|
||||
@@ -12,7 +12,7 @@ pub use gpui_macros::{
|
||||
use taffy::style::{AlignContent, Display};
|
||||
|
||||
/// A trait for elements that can be styled.
|
||||
/// Use this to opt-in to a CSS-like styling API.
|
||||
/// Use this to opt-in to a utility CSS-like styling API.
|
||||
pub trait Styled: Sized {
|
||||
/// Returns a reference to the style memory of this element.
|
||||
fn style(&mut self) -> &mut StyleRefinement;
|
||||
@@ -323,19 +323,23 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the text style that has been configured on this element.
|
||||
/// Returns a mutable reference to the text style that has been configured on this element.
|
||||
fn text_style(&mut self) -> &mut Option<TextStyleRefinement> {
|
||||
let style: &mut StyleRefinement = self.style();
|
||||
&mut style.text
|
||||
}
|
||||
|
||||
/// Set the text color of this element, this value cascades to its child elements.
|
||||
/// Sets the text color of this element.
|
||||
///
|
||||
/// This value cascades to its child elements.
|
||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.text_style().get_or_insert_with(Default::default).color = Some(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the font weight of this element, this value cascades to its child elements.
|
||||
/// Sets the font weight of this element
|
||||
///
|
||||
/// This value cascades to its child elements.
|
||||
fn font_weight(mut self, weight: FontWeight) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -343,7 +347,9 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the background color of this element, this value cascades to its child elements.
|
||||
/// Sets the background color of this element.
|
||||
///
|
||||
/// This value cascades to its child elements.
|
||||
fn text_bg(mut self, bg: impl Into<Hsla>) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -351,7 +357,9 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text size of this element, this value cascades to its child elements.
|
||||
/// Sets the text size of this element.
|
||||
///
|
||||
/// This value cascades to its child elements.
|
||||
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -359,8 +367,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text size to 'extra small',
|
||||
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
/// Sets the text size to 'extra small'.
|
||||
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
fn text_xs(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -368,8 +376,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text size to 'small',
|
||||
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
/// Sets the text size to 'small'.
|
||||
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
fn text_sm(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -377,7 +385,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Reset the text styling for this element and its children.
|
||||
/// Sets the text size to 'base'.
|
||||
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
fn text_base(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -385,8 +394,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text size to 'large',
|
||||
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
/// Sets the text size to 'large'.
|
||||
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
fn text_lg(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -394,8 +403,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text size to 'extra large',
|
||||
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
/// Sets the text size to 'extra large'.
|
||||
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
fn text_xl(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -403,8 +412,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text size to 'extra-extra large',
|
||||
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
/// Sets the text size to 'extra extra large'.
|
||||
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
fn text_2xl(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -412,8 +421,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text size to 'extra-extra-extra large',
|
||||
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
/// Sets the text size to 'extra extra extra large'.
|
||||
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
|
||||
fn text_3xl(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -421,17 +430,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the font style to 'non-italic',
|
||||
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-style#italicizing-text)
|
||||
fn non_italic(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_style = Some(FontStyle::Normal);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the font style to 'italic',
|
||||
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-style#italicizing-text)
|
||||
/// Sets the font style of the element to italic.
|
||||
/// [Docs](https://tailwindcss.com/docs/font-style#italicizing-text)
|
||||
fn italic(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -439,7 +439,29 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Remove the text decoration on this element, this value cascades to its child elements.
|
||||
/// Sets the font style of the element to normal (not italic).
|
||||
/// [Docs](https://tailwindcss.com/docs/font-style#italicizing-text)
|
||||
fn not_italic(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_style = Some(FontStyle::Normal);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the decoration of the text to have a line through it.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration#setting-the-text-decoration)
|
||||
fn line_through(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
style.strikethrough = Some(StrikethroughStyle {
|
||||
thickness: px(1.),
|
||||
..Default::default()
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Removes the text decoration on this element.
|
||||
///
|
||||
/// This value cascades to its child elements.
|
||||
fn text_decoration_none(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -447,7 +469,7 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the color for the underline on this element
|
||||
/// Sets the color for the underline on this element
|
||||
fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
@@ -455,7 +477,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the underline to a solid line
|
||||
/// Sets the text decoration style to a solid line.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration-style)
|
||||
fn text_decoration_solid(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
@@ -463,7 +486,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the underline to a wavy line
|
||||
/// Sets the text decoration style to a wavy line.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration-style)
|
||||
fn text_decoration_wavy(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
@@ -471,7 +495,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the underline to be 0 thickness, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
/// Sets the text decoration to be 0px thick.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
fn text_decoration_0(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
@@ -479,7 +504,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the underline to be 1px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
/// Sets the text decoration to be 1px thick.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
fn text_decoration_1(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
@@ -487,7 +513,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the underline to be 2px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
/// Sets the text decoration to be 2px thick.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
fn text_decoration_2(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
@@ -495,7 +522,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the underline to be 4px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
/// Sets the text decoration to be 4px thick.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
fn text_decoration_4(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
@@ -503,7 +531,8 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the underline to be 8px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
/// Sets the text decoration to be 8px thick.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
|
||||
fn text_decoration_8(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
@@ -511,7 +540,7 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the font family on this element and its children.
|
||||
/// Sets the font family of this element and its children.
|
||||
fn font_family(mut self, family_name: impl Into<SharedString>) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -519,7 +548,7 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the font of this element and its children.
|
||||
/// Sets the font of this element and its children.
|
||||
fn font(mut self, font: Font) -> Self {
|
||||
let Font {
|
||||
family,
|
||||
@@ -539,7 +568,7 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the line height on this element and its children.
|
||||
/// Sets the line height of this element and its children.
|
||||
fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -547,20 +576,20 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set opacity on this element and its children.
|
||||
/// Sets the opacity of this element and its children.
|
||||
fn opacity(mut self, opacity: f32) -> Self {
|
||||
self.style().opacity = Some(opacity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw a debug border around this element.
|
||||
/// Draws a debug border around this element.
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug(mut self) -> Self {
|
||||
self.style().debug = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw a debug border on all conforming elements below this element.
|
||||
/// Draws a debug border on all conforming elements below this element.
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_below(mut self) -> Self {
|
||||
self.style().debug_below = Some(true);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user