Compare commits
48 Commits
v0.135.2
...
edit-syste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e72b64547 | ||
|
|
45f12b9426 | ||
|
|
4f9ba28a25 | ||
|
|
0c2d71f1ac | ||
|
|
901cb8b3d2 | ||
|
|
9cef0ac869 | ||
|
|
79b5556267 | ||
|
|
dd67bda595 | ||
|
|
4762e52d31 | ||
|
|
50c45c7897 | ||
|
|
27ed0f4273 | ||
|
|
a3e75540af | ||
|
|
aa5113cd92 | ||
|
|
bca639bda9 | ||
|
|
bff1d8b142 | ||
|
|
95e246ac1c | ||
|
|
ba25e371be | ||
|
|
c73ef1a5f3 | ||
|
|
8b5a0cff10 | ||
|
|
f0af508ae5 | ||
|
|
5fe4070501 | ||
|
|
981a143e9b | ||
|
|
5e06ce4df3 | ||
|
|
3bd53d0441 | ||
|
|
76535578e9 | ||
|
|
fdcedf15b7 | ||
|
|
bd6d385817 | ||
|
|
5df1481297 | ||
|
|
ddaaaee973 | ||
|
|
9772b7ac33 | ||
|
|
2e0811e113 | ||
|
|
1b292d2fb3 | ||
|
|
adecbd1815 | ||
|
|
a7aa2578e1 | ||
|
|
24ffa0fcf3 | ||
|
|
b0494d1c05 | ||
|
|
a89dc8c42e | ||
|
|
d103903229 | ||
|
|
ec3aabe2c2 | ||
|
|
4b98c35d68 | ||
|
|
5103995c32 | ||
|
|
fb4c6dbaa7 | ||
|
|
91c1716858 | ||
|
|
0933426e63 | ||
|
|
689e4aef2f | ||
|
|
dbebb40956 | ||
|
|
d2cec0221b | ||
|
|
724acaab61 |
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@@ -104,23 +104,19 @@ jobs:
|
||||
# todo(linux): Actually run the tests
|
||||
linux_tests:
|
||||
name: (Linux) Run Clippy and tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
|
||||
@@ -130,7 +126,7 @@ jobs:
|
||||
# todo(windows): Actually run the tests
|
||||
windows_tests:
|
||||
name: (Windows) Run Clippy and tests
|
||||
runs-on: windows-latest
|
||||
runs-on: hosted-windows-1
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
@@ -262,26 +258,25 @@ jobs:
|
||||
|
||||
bundle-linux:
|
||||
name: Create a Linux bundle
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
14
.github/workflows/release_nightly.yml
vendored
14
.github/workflows/release_nightly.yml
vendored
@@ -96,7 +96,9 @@ jobs:
|
||||
bundle-deb:
|
||||
name: Create a Linux *.tar.gz bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
@@ -109,14 +111,8 @@ jobs:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
|
||||
9
.mailmap
9
.mailmap
@@ -15,8 +15,12 @@ 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>
|
||||
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>
|
||||
Ivan Žužak <izuzak@gmail.com>
|
||||
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
|
||||
Joseph T. Lyons <JosephTLyons@gmail.com>
|
||||
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
|
||||
Julia <floc@unpromptedtirade.com>
|
||||
@@ -29,6 +33,9 @@ Kirill Bulatov <kirill@zed.dev>
|
||||
Kirill Bulatov <kirill@zed.dev> <mail4score@gmail.com>
|
||||
Kyle Caverly <kylebcaverly@gmail.com>
|
||||
Kyle Caverly <kylebcaverly@gmail.com> <kyle@zed.dev>
|
||||
LoganDark <contact@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
|
||||
Marshall Bowers <elliott.codes@gmail.com>
|
||||
Marshall Bowers <elliott.codes@gmail.com> <marshall@zed.dev>
|
||||
Max Brunsfeld <maxbrunsfeld@gmail.com>
|
||||
@@ -41,6 +48,8 @@ Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
|
||||
Nathan Sobo <nathan@zed.dev>
|
||||
Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
|
||||
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
|
||||
Petros Amoiridis <petros@hey.com>
|
||||
Petros Amoiridis <petros@hey.com> <petros@zed.dev>
|
||||
Piotr Osiewicz <piotr@zed.dev>
|
||||
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
|
||||
Robert Clover <git@clo4.net>
|
||||
|
||||
133
Cargo.lock
generated
133
Cargo.lock
generated
@@ -377,6 +377,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"assistant_tooling",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
@@ -389,12 +390,12 @@ dependencies = [
|
||||
"language",
|
||||
"languages",
|
||||
"log",
|
||||
"nanoid",
|
||||
"node_runtime",
|
||||
"open_ai",
|
||||
"picker",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rich_text",
|
||||
"schemars",
|
||||
@@ -417,12 +418,15 @@ dependencies = [
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"log",
|
||||
"project",
|
||||
"repair_json",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sum_tree",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
]
|
||||
@@ -482,6 +486,7 @@ version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c"
|
||||
dependencies = [
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
@@ -815,6 +820,19 @@ dependencies = [
|
||||
"tungstenite 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async_zip"
|
||||
version = "0.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"crc32fast",
|
||||
"futures-lite 2.2.0",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
@@ -1490,7 +1508,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
|
||||
source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -1520,7 +1538,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
|
||||
source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2101,7 +2119,11 @@ dependencies = [
|
||||
"clap 4.4.4",
|
||||
"core-foundation",
|
||||
"core-services",
|
||||
"exec",
|
||||
"fork",
|
||||
"ipc-channel",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"plist",
|
||||
"release_channel",
|
||||
"serde",
|
||||
@@ -3110,6 +3132,12 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.6.1"
|
||||
@@ -3551,6 +3579,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.8"
|
||||
@@ -3561,6 +3600,16 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etagere"
|
||||
version = "0.2.8"
|
||||
@@ -3639,6 +3688,16 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exec"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615"
|
||||
dependencies = [
|
||||
"errno 0.2.8",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extension"
|
||||
version = "0.1.0"
|
||||
@@ -4039,6 +4098,15 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||
|
||||
[[package]]
|
||||
name = "fork"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60e74d3423998a57e9d906e49252fb79eb4a04d5cdfe188fb1b7ff9fc076a8ed"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@@ -4764,7 +4832,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"ctrlc",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
@@ -4776,6 +4843,7 @@ dependencies = [
|
||||
"rpc",
|
||||
"settings",
|
||||
"shellexpand",
|
||||
"signal-hook",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -5973,6 +6041,27 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"languages",
|
||||
"linkify",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"pulldown-cmark",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown_preview"
|
||||
version = "0.1.0"
|
||||
@@ -6339,15 +6428,20 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
"futures 0.3.28",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"walkdir",
|
||||
"windows 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8001,6 +8095,15 @@ dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "repair_json"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee191e184125fe72cb59b74160e25584e3908f2aaa84cbda1e161347102aa15"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.20"
|
||||
@@ -8341,7 +8444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"errno",
|
||||
"errno 0.3.8",
|
||||
"io-lifetimes 1.0.11",
|
||||
"libc",
|
||||
"linux-raw-sys 0.3.8",
|
||||
@@ -8355,7 +8458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"errno",
|
||||
"errno 0.3.8",
|
||||
"itoa",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.12",
|
||||
@@ -8369,7 +8472,7 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"errno 0.3.8",
|
||||
"libc",
|
||||
"rustix 0.38.32",
|
||||
]
|
||||
@@ -10136,18 +10239,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.48"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
|
||||
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.48"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
|
||||
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11293,9 +11396,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
@@ -12756,7 +12859,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.135.2"
|
||||
version = "0.136.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -12773,7 +12876,6 @@ dependencies = [
|
||||
"clap 4.4.4",
|
||||
"cli",
|
||||
"client",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette",
|
||||
@@ -12804,6 +12906,7 @@ dependencies = [
|
||||
"language_selector",
|
||||
"language_tools",
|
||||
"languages",
|
||||
"libc",
|
||||
"log",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
|
||||
21
Cargo.toml
21
Cargo.toml
@@ -4,8 +4,8 @@ members = [
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_tooling",
|
||||
"crates/assistant2",
|
||||
"crates/assistant_tooling",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -52,6 +52,7 @@ members = [
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/markdown",
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
@@ -192,6 +193,7 @@ languages = { path = "crates/languages" }
|
||||
live_kit_client = { path = "crates/live_kit_client" }
|
||||
live_kit_server = { path = "crates/live_kit_server" }
|
||||
lsp = { path = "crates/lsp" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
markdown_preview = { path = "crates/markdown_preview" }
|
||||
media = { path = "crates/media" }
|
||||
menu = { path = "crates/menu" }
|
||||
@@ -225,7 +227,7 @@ snippet = { path = "crates/snippet" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api"}
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
story = { path = "crates/story" }
|
||||
storybook = { path = "crates/storybook" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
@@ -255,20 +257,23 @@ async-fs = "1.6"
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
|
||||
cap-std = "3.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = { version = "0.11.6" }
|
||||
ctor = "0.2.6"
|
||||
ctrlc = "3.4.4"
|
||||
signal-hook = "0.3.17"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation-sys = "0.8.6"
|
||||
derive_more = "0.99.17"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.9"
|
||||
exec = "0.3.1"
|
||||
fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
@@ -287,10 +292,12 @@ isahc = { version = "1.7.2", default-features = false, features = [
|
||||
] }
|
||||
itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
nanoid = "0.4"
|
||||
nix = "0.28"
|
||||
once_cell = "1.19.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
@@ -304,6 +311,7 @@ pulldown-cmark = { version = "0.10.0", default-features = false }
|
||||
rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
||||
schemars = "0.8"
|
||||
@@ -383,6 +391,8 @@ version = "0.53.0"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"System",
|
||||
"System_Threading",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct2D",
|
||||
@@ -406,6 +416,7 @@ features = [
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Time",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_Ime",
|
||||
|
||||
@@ -117,6 +117,9 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"m": ["vim::PushOperator", "Mark"],
|
||||
"'": ["vim::PushOperator", { "Jump": { "line": true } }],
|
||||
"`": ["vim::PushOperator", { "Jump": { "line": false } }],
|
||||
";": "vim::RepeatFind",
|
||||
",": "vim::RepeatFindReversed",
|
||||
"ctrl-o": "pane::GoBack",
|
||||
@@ -237,6 +240,9 @@
|
||||
],
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
"g i": ["workspace::SendKeystrokes", "` ^ i"],
|
||||
"g ,": "vim::ChangeListNewer",
|
||||
"g ;": "vim::ChangeListOlder",
|
||||
"shift-h": "vim::WindowTop",
|
||||
"shift-m": "vim::WindowMiddle",
|
||||
"shift-l": "vim::WindowBottom",
|
||||
|
||||
@@ -316,6 +316,8 @@
|
||||
"autosave": "off",
|
||||
// Settings related to the editor's tab bar.
|
||||
"tab_bar": {
|
||||
// Whether or not to show the tab bar in the editor
|
||||
"show": true,
|
||||
// Whether or not to show the navigation history buttons.
|
||||
"show_nav_history_buttons": true
|
||||
},
|
||||
@@ -626,6 +628,9 @@
|
||||
"Make": {
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off"
|
||||
},
|
||||
"Prisma": {
|
||||
"tab_size": 2
|
||||
}
|
||||
|
||||
@@ -281,11 +281,14 @@ impl ActivityIndicator {
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
AutoUpdateStatus::Updated { binary_path } => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
workspace::restart(&Default::default(), cx)
|
||||
on_click: Some(Arc::new({
|
||||
let restart = workspace::Restart {
|
||||
binary_path: Some(binary_path.clone()),
|
||||
};
|
||||
move |_, cx| workspace::restart(&restart, cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
|
||||
@@ -19,6 +19,7 @@ stories = ["dep:story"]
|
||||
anyhow.workspace = true
|
||||
assistant_tooling.workspace = true
|
||||
client.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
@@ -28,10 +29,10 @@ fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rich_text.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_index.workspace = true
|
||||
|
||||
@@ -2,22 +2,18 @@ mod assistant_settings;
|
||||
mod attachments;
|
||||
mod completion_provider;
|
||||
mod saved_conversation;
|
||||
mod saved_conversation_picker;
|
||||
mod saved_conversations;
|
||||
mod tools;
|
||||
pub mod ui;
|
||||
|
||||
use crate::saved_conversation::{SavedConversation, SavedMessage, SavedMessageRole};
|
||||
use crate::saved_conversation_picker::SavedConversationPicker;
|
||||
use crate::{
|
||||
attachments::ActiveEditorAttachmentTool,
|
||||
tools::{CreateBufferTool, ProjectIndexTool},
|
||||
ui::UserOrAssistant,
|
||||
};
|
||||
use crate::saved_conversation::SavedConversationMetadata;
|
||||
use crate::ui::UserOrAssistant;
|
||||
use ::ui::{div, prelude::*, Color, Tooltip, ViewContext};
|
||||
use anyhow::{Context, Result};
|
||||
use assistant_tooling::{
|
||||
AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry, UserAttachment,
|
||||
};
|
||||
use attachments::ActiveEditorAttachmentTool;
|
||||
use client::{proto, Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use completion_provider::*;
|
||||
@@ -32,11 +28,13 @@ use gpui::{
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
|
||||
use rich_text::RichText;
|
||||
use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation};
|
||||
use saved_conversations::SavedConversations;
|
||||
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use tools::AnnotationTool;
|
||||
use tools::{AnnotationTool, CreateBufferTool, ProjectIndexTool};
|
||||
use ui::{ActiveFileButton, Composer, ProjectIndexButton};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
|
||||
@@ -50,28 +48,9 @@ pub use assistant_settings::AssistantSettings;
|
||||
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
|
||||
pub struct Submit(SubmitMode);
|
||||
pub struct Submit;
|
||||
|
||||
/// There are multiple different ways to submit a model request, represented by this enum.
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
|
||||
pub enum SubmitMode {
|
||||
/// Only include the conversation.
|
||||
Simple,
|
||||
/// Send the current file as context.
|
||||
CurrentFile,
|
||||
/// Search the codebase and send relevant excerpts.
|
||||
Codebase,
|
||||
}
|
||||
|
||||
gpui::actions!(
|
||||
assistant2,
|
||||
[
|
||||
Cancel,
|
||||
ToggleFocus,
|
||||
DebugProjectIndex,
|
||||
ToggleSavedConversations
|
||||
]
|
||||
);
|
||||
gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex,]);
|
||||
gpui::impl_actions!(assistant2, [Submit]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
@@ -111,8 +90,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
cx.observe_new_views(SavedConversationPicker::register)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn enabled(cx: &AppContext) -> bool {
|
||||
@@ -141,16 +118,13 @@ impl AssistantPanel {
|
||||
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(ProjectIndexTool::new(project_index.clone()), cx)
|
||||
.register(ProjectIndexTool::new(project_index.clone()))
|
||||
.unwrap();
|
||||
tool_registry
|
||||
.register(
|
||||
CreateBufferTool::new(workspace.clone(), project.clone()),
|
||||
cx,
|
||||
)
|
||||
.register(CreateBufferTool::new(workspace.clone(), project.clone()))
|
||||
.unwrap();
|
||||
tool_registry
|
||||
.register(AnnotationTool::new(workspace.clone(), project.clone()), cx)
|
||||
.register(AnnotationTool::new(workspace.clone(), project.clone()))
|
||||
.unwrap();
|
||||
|
||||
let mut attachment_registry = AttachmentRegistry::new();
|
||||
@@ -264,6 +238,8 @@ pub struct AssistantChat {
|
||||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
composer_editor: View<Editor>,
|
||||
saved_conversations: View<SavedConversations>,
|
||||
saved_conversations_open: bool,
|
||||
project_index_button: View<ProjectIndexButton>,
|
||||
active_file_button: Option<View<ActiveFileButton>>,
|
||||
user_store: Model<UserStore>,
|
||||
@@ -319,6 +295,24 @@ impl AssistantChat {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let saved_conversations = cx.new_view(|cx| SavedConversations::new(cx));
|
||||
cx.spawn({
|
||||
let fs = fs.clone();
|
||||
let saved_conversations = saved_conversations.downgrade();
|
||||
|_assistant_chat, mut cx| async move {
|
||||
let saved_conversation_metadata = SavedConversationMetadata::list(fs).await?;
|
||||
|
||||
cx.update(|cx| {
|
||||
saved_conversations.update(cx, |this, cx| {
|
||||
this.init(saved_conversation_metadata, cx);
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
model,
|
||||
messages: Vec::new(),
|
||||
@@ -328,6 +322,8 @@ impl AssistantChat {
|
||||
editor.set_placeholder_text("Send a message…", cx);
|
||||
editor
|
||||
}),
|
||||
saved_conversations,
|
||||
saved_conversations_open: false,
|
||||
list_state,
|
||||
user_store,
|
||||
fs,
|
||||
@@ -359,6 +355,10 @@ impl AssistantChat {
|
||||
})
|
||||
}
|
||||
|
||||
fn toggle_saved_conversations(&mut self) {
|
||||
self.saved_conversations_open = !self.saved_conversations_open;
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
// If we're currently editing a message, cancel the edit.
|
||||
if let Some(editing_message) = self.editing_message.take() {
|
||||
@@ -380,7 +380,7 @@ impl AssistantChat {
|
||||
cx.propagate();
|
||||
}
|
||||
|
||||
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
|
||||
fn submit(&mut self, _: &Submit, cx: &mut ViewContext<Self>) {
|
||||
if let Some(focused_message_id) = self.focused_message_id(cx) {
|
||||
self.truncate_messages(focused_message_id, cx);
|
||||
self.pending_completion.take();
|
||||
@@ -418,7 +418,6 @@ impl AssistantChat {
|
||||
return;
|
||||
}
|
||||
|
||||
let mode = *mode;
|
||||
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
|
||||
let attachments_task = this.update(&mut cx, |this, cx| {
|
||||
let attachment_registry = this.attachment_registry.clone();
|
||||
@@ -443,14 +442,9 @@ impl AssistantChat {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Self::request_completion(
|
||||
this.clone(),
|
||||
mode,
|
||||
MAX_COMPLETION_CALLS_PER_SUBMISSION,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
Self::request_completion(this.clone(), MAX_COMPLETION_CALLS_PER_SUBMISSION, &mut cx)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.pending_completion = None;
|
||||
@@ -462,7 +456,6 @@ impl AssistantChat {
|
||||
|
||||
async fn request_completion(
|
||||
this: WeakView<Self>,
|
||||
mode: SubmitMode,
|
||||
limit: usize,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
@@ -472,9 +465,7 @@ impl AssistantChat {
|
||||
let (tool_definitions, model_name, messages) = this.update(cx, |this, cx| {
|
||||
this.push_new_assistant_message(cx);
|
||||
|
||||
let definitions = if call_count < limit
|
||||
&& matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
|
||||
{
|
||||
let definitions = if call_count < limit {
|
||||
this.tool_registry.definitions()
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -505,13 +496,11 @@ impl AssistantChat {
|
||||
while let Some(delta) = stream.next().await {
|
||||
let delta = delta?;
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(ChatMessage::Assistant(GroupedAssistantMessage {
|
||||
messages,
|
||||
..
|
||||
})) = this.messages.last_mut()
|
||||
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
|
||||
this.messages.last_mut()
|
||||
{
|
||||
if messages.is_empty() {
|
||||
messages.push(AssistantMessage {
|
||||
messages.push(AssistantMessagePart {
|
||||
body: RichText::default(),
|
||||
tool_calls: Vec::new(),
|
||||
})
|
||||
@@ -523,25 +512,27 @@ impl AssistantChat {
|
||||
body.push_str(content);
|
||||
}
|
||||
|
||||
for tool_call in delta.tool_calls {
|
||||
let index = tool_call.index as usize;
|
||||
for tool_call_delta in delta.tool_calls {
|
||||
let index = tool_call_delta.index as usize;
|
||||
if index >= message.tool_calls.len() {
|
||||
message.tool_calls.resize_with(index + 1, Default::default);
|
||||
}
|
||||
let call = &mut message.tool_calls[index];
|
||||
let tool_call = &mut message.tool_calls[index];
|
||||
|
||||
if let Some(id) = &tool_call.id {
|
||||
call.id.push_str(id);
|
||||
if let Some(id) = &tool_call_delta.id {
|
||||
tool_call.id.push_str(id);
|
||||
}
|
||||
|
||||
match tool_call.variant {
|
||||
Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
|
||||
if let Some(name) = &tool_call.name {
|
||||
call.name.push_str(name);
|
||||
}
|
||||
if let Some(arguments) = &tool_call.arguments {
|
||||
call.arguments.push_str(arguments);
|
||||
}
|
||||
match tool_call_delta.variant {
|
||||
Some(proto::tool_call_delta::Variant::Function(
|
||||
tool_call_delta,
|
||||
)) => {
|
||||
this.tool_registry.update_tool_call(
|
||||
tool_call,
|
||||
tool_call_delta.name.as_deref(),
|
||||
tool_call_delta.arguments.as_deref(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
@@ -562,7 +553,7 @@ impl AssistantChat {
|
||||
|
||||
let mut tool_tasks = Vec::new();
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(ChatMessage::Assistant(GroupedAssistantMessage {
|
||||
if let Some(ChatMessage::Assistant(AssistantMessage {
|
||||
error: message_error,
|
||||
messages,
|
||||
..
|
||||
@@ -573,53 +564,39 @@ impl AssistantChat {
|
||||
cx.notify();
|
||||
} else {
|
||||
if let Some(current_message) = messages.last_mut() {
|
||||
for tool_call in current_message.tool_calls.iter() {
|
||||
tool_tasks.push(this.tool_registry.call(tool_call, cx));
|
||||
for tool_call in current_message.tool_calls.iter_mut() {
|
||||
tool_tasks
|
||||
.extend(this.tool_registry.execute_tool_call(tool_call, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
// This ends recursion on calling for responses after tools
|
||||
if tool_tasks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tools = join_all(tool_tasks.into_iter()).await;
|
||||
// If the WindowContext went away for any tool's view we don't include it
|
||||
// especially since the below call would fail for the same reason.
|
||||
let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(ChatMessage::Assistant(GroupedAssistantMessage { messages, .. })) =
|
||||
this.messages.last_mut()
|
||||
{
|
||||
if let Some(current_message) = messages.last_mut() {
|
||||
current_message.tool_calls = tools;
|
||||
cx.notify();
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
})?;
|
||||
join_all(tool_tasks.into_iter()).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// If the last message is a grouped assistant message, add to the grouped message
|
||||
if let Some(ChatMessage::Assistant(GroupedAssistantMessage { messages, .. })) =
|
||||
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
|
||||
self.messages.last_mut()
|
||||
{
|
||||
messages.push(AssistantMessage {
|
||||
messages.push(AssistantMessagePart {
|
||||
body: RichText::default(),
|
||||
tool_calls: Vec::new(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let message = ChatMessage::Assistant(GroupedAssistantMessage {
|
||||
let message = ChatMessage::Assistant(AssistantMessage {
|
||||
id: self.next_message_id.post_inc(),
|
||||
messages: vec![AssistantMessage {
|
||||
messages: vec![AssistantMessagePart {
|
||||
body: RichText::default(),
|
||||
tool_calls: Vec::new(),
|
||||
}],
|
||||
@@ -668,40 +645,30 @@ impl AssistantChat {
|
||||
*entry = !*entry;
|
||||
}
|
||||
|
||||
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let messages = self
|
||||
.messages
|
||||
.drain(..)
|
||||
.map(|message| {
|
||||
let text = match &message {
|
||||
ChatMessage::User(message) => message.body.read(cx).text(cx),
|
||||
ChatMessage::Assistant(message) => message
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.body.text.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n"),
|
||||
};
|
||||
|
||||
SavedMessage {
|
||||
id: message.id(),
|
||||
role: match message {
|
||||
ChatMessage::User(_) => SavedMessageRole::User,
|
||||
ChatMessage::Assistant(_) => SavedMessageRole::Assistant,
|
||||
},
|
||||
text,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Reset the chat for the new conversation.
|
||||
fn reset(&mut self) {
|
||||
self.messages.clear();
|
||||
self.list_state.reset(0);
|
||||
self.editing_message.take();
|
||||
self.collapsed_messages.clear();
|
||||
}
|
||||
|
||||
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let messages = std::mem::take(&mut self.messages)
|
||||
.into_iter()
|
||||
.map(|message| self.serialize_message(message, cx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.reset();
|
||||
|
||||
let title = messages
|
||||
.first()
|
||||
.map(|message| message.text.clone())
|
||||
.map(|message| match message {
|
||||
SavedChatMessage::User { body, .. } => body.clone(),
|
||||
SavedChatMessage::Assistant { messages, .. } => messages
|
||||
.first()
|
||||
.map(|message| message.body.to_string())
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
.unwrap_or_else(|| "A conversation with the assistant.".to_string());
|
||||
|
||||
let saved_conversation = SavedConversation {
|
||||
@@ -835,7 +802,7 @@ impl AssistantChat {
|
||||
}
|
||||
})
|
||||
.into_any(),
|
||||
ChatMessage::Assistant(GroupedAssistantMessage {
|
||||
ChatMessage::Assistant(AssistantMessage {
|
||||
id,
|
||||
messages,
|
||||
error,
|
||||
@@ -856,7 +823,7 @@ impl AssistantChat {
|
||||
let tools = message
|
||||
.tool_calls
|
||||
.iter()
|
||||
.map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
|
||||
.filter_map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
|
||||
.collect::<Vec<AnyElement>>();
|
||||
|
||||
if !tools.is_empty() {
|
||||
@@ -864,6 +831,10 @@ impl AssistantChat {
|
||||
}
|
||||
}
|
||||
|
||||
if message_elements.is_empty() {
|
||||
message_elements.push(::ui::Label::new("Researching...").into_any_element())
|
||||
}
|
||||
|
||||
div()
|
||||
.when(is_first, |this| this.pt(padding))
|
||||
.child(
|
||||
@@ -896,6 +867,21 @@ impl AssistantChat {
|
||||
let mut project_context = ProjectContext::new(project, fs);
|
||||
let mut completion_messages = Vec::new();
|
||||
|
||||
completion_messages.push(CompletionMessage::System {
|
||||
content: r#"
|
||||
You are the assistant for the Zed code editor.
|
||||
Your job is to help the user understand and modify their own code.
|
||||
Use tools to retrieve the information needed to give answers that are
|
||||
specific to the user's codebase. Do NOT give generic answers that are
|
||||
not specific to the user's codebase.
|
||||
Whenever possible, use tools to display code in the editor.
|
||||
"#
|
||||
.lines()
|
||||
.map(|line| line.trim_start())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect(),
|
||||
});
|
||||
|
||||
for message in &self.messages {
|
||||
match message {
|
||||
ChatMessage::User(UserMessage {
|
||||
@@ -912,7 +898,7 @@ impl AssistantChat {
|
||||
content: body.read(cx).text(cx),
|
||||
});
|
||||
}
|
||||
ChatMessage::Assistant(GroupedAssistantMessage { messages, .. }) => {
|
||||
ChatMessage::Assistant(AssistantMessage { messages, .. }) => {
|
||||
for message in messages {
|
||||
let body = message.body.clone();
|
||||
|
||||
@@ -941,13 +927,11 @@ impl AssistantChat {
|
||||
|
||||
for tool_call in &message.tool_calls {
|
||||
// Every tool call _must_ have a result by ID, otherwise OpenAI will error.
|
||||
let content = match &tool_call.result {
|
||||
Some(result) => {
|
||||
result.generate(&tool_call.name, &mut project_context, cx)
|
||||
}
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
let content = self.tool_registry.content_for_tool_call(
|
||||
tool_call,
|
||||
&mut project_context,
|
||||
cx,
|
||||
);
|
||||
completion_messages.push(CompletionMessage::Tool {
|
||||
content,
|
||||
tool_call_id: tool_call.id.clone(),
|
||||
@@ -966,10 +950,53 @@ impl AssistantChat {
|
||||
Ok(completion_messages)
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_message(
|
||||
&self,
|
||||
message: ChatMessage,
|
||||
cx: &mut ViewContext<AssistantChat>,
|
||||
) -> SavedChatMessage {
|
||||
match message {
|
||||
ChatMessage::User(message) => SavedChatMessage::User {
|
||||
id: message.id,
|
||||
body: message.body.read(cx).text(cx),
|
||||
attachments: message
|
||||
.attachments
|
||||
.iter()
|
||||
.map(|attachment| {
|
||||
self.attachment_registry
|
||||
.serialize_user_attachment(attachment)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
ChatMessage::Assistant(message) => SavedChatMessage::Assistant {
|
||||
id: message.id,
|
||||
error: message.error,
|
||||
messages: message
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| SavedAssistantMessagePart {
|
||||
body: message.body.text.clone(),
|
||||
tool_calls: message
|
||||
.tool_calls
|
||||
.iter()
|
||||
.filter_map(|tool_call| {
|
||||
self.tool_registry
|
||||
.serialize_tool_call(tool_call, cx)
|
||||
.log_err()
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantChat {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let header_height = Spacing::Small.rems(cx) * 2.0 + ButtonSize::Default.rems();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.flex_1()
|
||||
@@ -978,23 +1005,60 @@ impl Render for AssistantChat {
|
||||
.on_action(cx.listener(Self::submit))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.text_color(Color::Default.color(cx))
|
||||
.child(list(self.list_state.clone()).flex_1().pt(header_height))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.h(header_height)
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.child(
|
||||
Button::new("open-saved-conversations", "Saved Conversations").on_click(
|
||||
|_event, cx| cx.dispatch_action(Box::new(ToggleSavedConversations)),
|
||||
),
|
||||
IconButton::new(
|
||||
"toggle-saved-conversations",
|
||||
if self.saved_conversations_open {
|
||||
IconName::ChevronRight
|
||||
} else {
|
||||
IconName::ChevronLeft
|
||||
},
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, _cx| {
|
||||
this.toggle_saved_conversations();
|
||||
}))
|
||||
.tooltip(move |cx| Tooltip::text("Switch Conversations", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("new-conversation", IconName::Plus)
|
||||
.on_click(cx.listener(move |this, _event, cx| {
|
||||
this.new_conversation(cx);
|
||||
}))
|
||||
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
|
||||
h_flex()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.child(
|
||||
IconButton::new("new-conversation", IconName::Plus)
|
||||
.on_click(cx.listener(move |this, _event, cx| {
|
||||
this.new_conversation(cx);
|
||||
}))
|
||||
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("assistant-menu", IconName::Menu)
|
||||
.disabled(true)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
"Coming soon – Assistant settings & controls",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(list(self.list_state.clone()).flex_1())
|
||||
.when(self.saved_conversations_open, |element| {
|
||||
element.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top(header_height)
|
||||
.w_full()
|
||||
.child(self.saved_conversations.clone()),
|
||||
)
|
||||
})
|
||||
.child(Composer::new(
|
||||
self.composer_editor.clone(),
|
||||
self.project_index_button.clone(),
|
||||
@@ -1018,17 +1082,10 @@ impl MessageId {
|
||||
|
||||
enum ChatMessage {
|
||||
User(UserMessage),
|
||||
Assistant(GroupedAssistantMessage),
|
||||
Assistant(AssistantMessage),
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
pub fn id(&self) -> MessageId {
|
||||
match self {
|
||||
ChatMessage::User(message) => message.id,
|
||||
ChatMessage::Assistant(message) => message.id,
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
|
||||
match self {
|
||||
ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
|
||||
@@ -1038,18 +1095,18 @@ impl ChatMessage {
|
||||
}
|
||||
|
||||
struct UserMessage {
|
||||
id: MessageId,
|
||||
body: View<Editor>,
|
||||
attachments: Vec<UserAttachment>,
|
||||
pub id: MessageId,
|
||||
pub body: View<Editor>,
|
||||
pub attachments: Vec<UserAttachment>,
|
||||
}
|
||||
|
||||
struct AssistantMessagePart {
|
||||
pub body: RichText,
|
||||
pub tool_calls: Vec<ToolFunctionCall>,
|
||||
}
|
||||
|
||||
struct AssistantMessage {
|
||||
body: RichText,
|
||||
tool_calls: Vec<ToolFunctionCall>,
|
||||
}
|
||||
|
||||
struct GroupedAssistantMessage {
|
||||
id: MessageId,
|
||||
messages: Vec<AssistantMessage>,
|
||||
error: Option<SharedString>,
|
||||
pub id: MessageId,
|
||||
pub messages: Vec<AssistantMessagePart>,
|
||||
pub error: Option<SharedString>,
|
||||
}
|
||||
|
||||
@@ -1,64 +1,68 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tooling::{LanguageModelAttachment, ProjectContext, ToolOutput};
|
||||
use assistant_tooling::{AttachmentOutput, LanguageModelAttachment, ProjectContext};
|
||||
use editor::Editor;
|
||||
use gpui::{Render, Task, View, WeakModel, WeakView};
|
||||
use language::Buffer;
|
||||
use project::ProjectPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
|
||||
use util::maybe;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ActiveEditorAttachment {
|
||||
buffer: WeakModel<Buffer>,
|
||||
path: Option<ProjectPath>,
|
||||
#[serde(skip)]
|
||||
buffer: Option<WeakModel<Buffer>>,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct FileAttachmentView {
|
||||
output: Result<ActiveEditorAttachment>,
|
||||
project_path: Option<ProjectPath>,
|
||||
buffer: Option<WeakModel<Buffer>>,
|
||||
error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
impl Render for FileAttachmentView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
match &self.output {
|
||||
Ok(attachment) => {
|
||||
let filename: SharedString = attachment
|
||||
.path
|
||||
.as_ref()
|
||||
.and_then(|p| p.path.file_name()?.to_str())
|
||||
.unwrap_or("Untitled")
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
// todo!(): make the button link to the actual file to open
|
||||
ButtonLike::new("file-attachment")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(ui::Icon::new(IconName::File))
|
||||
.child(filename.clone()),
|
||||
)
|
||||
.tooltip({
|
||||
move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
Err(err) => div().child(err.to_string()).into_any_element(),
|
||||
if let Some(error) = &self.error {
|
||||
return div().child(error.to_string()).into_any_element();
|
||||
}
|
||||
|
||||
let filename: SharedString = self
|
||||
.project_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.path.file_name()?.to_str())
|
||||
.unwrap_or("Untitled")
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
ButtonLike::new("file-attachment")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(ui::Icon::new(IconName::File))
|
||||
.child(filename.clone()),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for FileAttachmentView {
|
||||
impl AttachmentOutput for FileAttachmentView {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
|
||||
if let Ok(result) = &self.output {
|
||||
if let Some(path) = &result.path {
|
||||
project.add_file(path.clone());
|
||||
return format!("current file: {}", path.path.display());
|
||||
} else if let Some(buffer) = result.buffer.upgrade() {
|
||||
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
|
||||
}
|
||||
if let Some(path) = &self.project_path {
|
||||
project.add_file(path.clone());
|
||||
return format!("current file: {}", path.path.display());
|
||||
}
|
||||
|
||||
if let Some(buffer) = self.buffer.as_ref().and_then(|buffer| buffer.upgrade()) {
|
||||
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
@@ -77,6 +81,10 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
|
||||
type Output = ActiveEditorAttachment;
|
||||
type View = FileAttachmentView;
|
||||
|
||||
fn name(&self) -> Arc<str> {
|
||||
"active-editor-attachment".into()
|
||||
}
|
||||
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
|
||||
Task::ready(maybe!({
|
||||
let active_buffer = self
|
||||
@@ -91,13 +99,10 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
|
||||
let buffer = active_buffer.read(cx);
|
||||
|
||||
if let Some(buffer) = buffer.as_singleton() {
|
||||
let path =
|
||||
project::File::from_dyn(buffer.read(cx).file()).map(|file| ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
});
|
||||
let path = project::File::from_dyn(buffer.read(cx).file())
|
||||
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok());
|
||||
return Ok(ActiveEditorAttachment {
|
||||
buffer: buffer.downgrade(),
|
||||
buffer: Some(buffer.downgrade()),
|
||||
path,
|
||||
});
|
||||
} else {
|
||||
@@ -106,7 +111,34 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
|
||||
}))
|
||||
}
|
||||
|
||||
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| FileAttachmentView { output })
|
||||
fn view(
|
||||
&self,
|
||||
output: Result<ActiveEditorAttachment>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
let error;
|
||||
let project_path;
|
||||
let buffer;
|
||||
match output {
|
||||
Ok(output) => {
|
||||
error = None;
|
||||
let workspace = self.workspace.upgrade().unwrap();
|
||||
let project = workspace.read(cx).project();
|
||||
project_path = output
|
||||
.path
|
||||
.and_then(|path| project.read(cx).project_path_for_absolute_path(&path, cx));
|
||||
buffer = output.buffer;
|
||||
}
|
||||
Err(err) => {
|
||||
error = Some(err);
|
||||
buffer = None;
|
||||
project_path = None;
|
||||
}
|
||||
}
|
||||
cx.new_view(|_cx| FileAttachmentView {
|
||||
project_path,
|
||||
buffer,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::SharedString;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
use crate::MessageId;
|
||||
|
||||
@@ -8,42 +20,71 @@ pub struct SavedConversation {
|
||||
pub version: String,
|
||||
/// The title of the conversation, generated by the Assistant.
|
||||
pub title: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub messages: Vec<SavedChatMessage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum SavedMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
pub enum SavedChatMessage {
|
||||
User {
|
||||
id: MessageId,
|
||||
body: String,
|
||||
attachments: Vec<SavedUserAttachment>,
|
||||
},
|
||||
Assistant {
|
||||
id: MessageId,
|
||||
messages: Vec<SavedAssistantMessagePart>,
|
||||
error: Option<SharedString>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub role: SavedMessageRole,
|
||||
pub text: String,
|
||||
pub struct SavedAssistantMessagePart {
|
||||
pub body: SharedString,
|
||||
pub tool_calls: Vec<SavedToolFunctionCall>,
|
||||
}
|
||||
|
||||
/// Returns a list of placeholder conversations for mocking the UI.
|
||||
///
|
||||
/// Once we have real saved conversations to pull from we can use those instead.
|
||||
pub fn placeholder_conversations() -> Vec<SavedConversation> {
|
||||
vec![
|
||||
SavedConversation {
|
||||
version: "0.3.0".to_string(),
|
||||
title: "How to get a list of exported functions in an Erlang module".to_string(),
|
||||
messages: vec![],
|
||||
},
|
||||
SavedConversation {
|
||||
version: "0.3.0".to_string(),
|
||||
title: "7 wonders of the ancient world".to_string(),
|
||||
messages: vec![],
|
||||
},
|
||||
SavedConversation {
|
||||
version: "0.3.0".to_string(),
|
||||
title: "Size difference between u8 and a reference to u8 in Rust".to_string(),
|
||||
messages: vec![],
|
||||
},
|
||||
]
|
||||
pub struct SavedConversationMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.\d.\d.\d.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out conversations saved by the old assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,57 +5,66 @@ use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, V
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::saved_conversation::{self, SavedConversation};
|
||||
use crate::ToggleSavedConversations;
|
||||
use crate::saved_conversation::SavedConversationMetadata;
|
||||
|
||||
pub struct SavedConversationPicker {
|
||||
picker: View<Picker<SavedConversationPickerDelegate>>,
|
||||
pub struct SavedConversations {
|
||||
focus_handle: FocusHandle,
|
||||
picker: Option<View<Picker<SavedConversationPickerDelegate>>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for SavedConversationPicker {}
|
||||
impl EventEmitter<DismissEvent> for SavedConversations {}
|
||||
|
||||
impl ModalView for SavedConversationPicker {}
|
||||
|
||||
impl FocusableView for SavedConversationPicker {
|
||||
impl FocusableView for SavedConversations {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
if let Some(picker) = self.picker.as_ref() {
|
||||
picker.focus_handle(cx)
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SavedConversationPicker {
|
||||
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|workspace, _: &ToggleSavedConversations, cx| {
|
||||
workspace.toggle_modal(cx, move |cx| {
|
||||
let delegate = SavedConversationPickerDelegate::new(cx.view().downgrade());
|
||||
Self::new(delegate, cx)
|
||||
});
|
||||
});
|
||||
impl SavedConversations {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
picker: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(delegate: SavedConversationPickerDelegate, cx: &mut ViewContext<Self>) -> Self {
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
Self { picker }
|
||||
pub fn init(
|
||||
&mut self,
|
||||
saved_conversations: Vec<SavedConversationMetadata>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let delegate =
|
||||
SavedConversationPickerDelegate::new(cx.view().downgrade(), saved_conversations);
|
||||
self.picker = Some(cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)));
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SavedConversationPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
impl Render for SavedConversations {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.children(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SavedConversationPickerDelegate {
|
||||
view: WeakView<SavedConversationPicker>,
|
||||
saved_conversations: Vec<SavedConversation>,
|
||||
view: WeakView<SavedConversations>,
|
||||
saved_conversations: Vec<SavedConversationMetadata>,
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
}
|
||||
|
||||
impl SavedConversationPickerDelegate {
|
||||
pub fn new(weak_view: WeakView<SavedConversationPicker>) -> Self {
|
||||
let saved_conversations = saved_conversation::placeholder_conversations();
|
||||
pub fn new(
|
||||
weak_view: WeakView<SavedConversations>,
|
||||
saved_conversations: Vec<SavedConversationMetadata>,
|
||||
) -> Self {
|
||||
let matches = saved_conversations
|
||||
.iter()
|
||||
.map(|conversation| StringMatch {
|
||||
@@ -176,7 +185,6 @@ impl PickerDelegate for SavedConversationPickerDelegate {
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
@@ -4,9 +4,10 @@ use editor::{
|
||||
display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle},
|
||||
Editor, MultiBuffer,
|
||||
};
|
||||
use gpui::{prelude::*, AnyElement, Model, Task, View, WeakView};
|
||||
use futures::{channel::mpsc::UnboundedSender, StreamExt as _};
|
||||
use gpui::{prelude::*, AnyElement, AsyncWindowContext, Model, Task, View, WeakView};
|
||||
use language::ToPoint;
|
||||
use project::{Project, ProjectPath};
|
||||
use project::{search::SearchQuery, Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
@@ -25,135 +26,177 @@ impl AnnotationTool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema, Clone)]
|
||||
#[derive(Default, Debug, Deserialize, JsonSchema, Clone)]
|
||||
pub struct AnnotationInput {
|
||||
/// Name for this set of annotations
|
||||
#[serde(default = "default_title")]
|
||||
title: String,
|
||||
annotations: Vec<Annotation>,
|
||||
/// Excerpts from the file to show to the user.
|
||||
excerpts: Vec<Excerpt>,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
"Untitled".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema, Clone)]
|
||||
struct Annotation {
|
||||
struct Excerpt {
|
||||
/// Path to the file
|
||||
path: String,
|
||||
/// Name of a symbol in the code
|
||||
symbol_name: String,
|
||||
/// Text to display near the symbol definition
|
||||
text: String,
|
||||
/// A short, distinctive string that appears in the file, used to define a location in the file.
|
||||
text_passage: String,
|
||||
/// Text to display above the code excerpt. All explanation of code should be included here.
|
||||
annotation: String,
|
||||
}
|
||||
|
||||
impl LanguageModelTool for AnnotationTool {
|
||||
type Input = AnnotationInput;
|
||||
type Output = String;
|
||||
type View = AnnotationResultView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"annotate_code".to_string()
|
||||
"show_code_file_excerpts".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Dynamically annotate symbols in the current codebase. Opens a buffer in a panel in their editor, to the side of the conversation. The annotations are shown in the editor as a block decoration.".to_string()
|
||||
"
|
||||
Show and explain code from the current project
|
||||
Opens a buffer in a separate pane/tab, to the side of the conversation.
|
||||
The annotations are shown in the editor as block decorations.
|
||||
Many related excerpts can be shown at once.
|
||||
"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let excerpts = input.annotations.clone();
|
||||
let title = input.title.clone();
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|cx| {
|
||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||
cx.spawn(|view, mut cx| async move {
|
||||
while let Some(excerpt) = rx.next().await {
|
||||
AnnotationResultView::add_excerpt(view.clone(), excerpt, &mut cx).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
AnnotationResultView {
|
||||
project: self.project.clone(),
|
||||
workspace: self.workspace.clone(),
|
||||
tx,
|
||||
pending_excerpt: None,
|
||||
added_editor_to_workspace: false,
|
||||
editor: None,
|
||||
error: None,
|
||||
rendered_excerpt_count: 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnnotationResultView {
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
pending_excerpt: Option<Excerpt>,
|
||||
added_editor_to_workspace: bool,
|
||||
editor: Option<View<Editor>>,
|
||||
tx: UnboundedSender<Excerpt>,
|
||||
error: Option<anyhow::Error>,
|
||||
rendered_excerpt_count: usize,
|
||||
}
|
||||
|
||||
impl AnnotationResultView {
|
||||
async fn add_excerpt(
|
||||
this: WeakView<Self>,
|
||||
excerpt: Excerpt,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let project = this.update(cx, |this, _cx| this.project.clone())?;
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
let worktree = project.worktrees().next()?;
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
Some(worktree_id)
|
||||
});
|
||||
})?;
|
||||
|
||||
let worktree_id = if let Some(worktree_id) = worktree_id {
|
||||
worktree_id
|
||||
} else {
|
||||
return Task::ready(Err(anyhow::anyhow!("No worktree found")));
|
||||
return Err(anyhow::anyhow!("No worktree found"));
|
||||
};
|
||||
|
||||
let buffer_tasks = project.update(cx, |project, cx| {
|
||||
let excerpts = excerpts.clone();
|
||||
excerpts
|
||||
.iter()
|
||||
.map(|excerpt| {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new(&excerpt.path).into(),
|
||||
};
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
let buffer_task = project.update(cx, |project, cx| {
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new(&excerpt.path).into(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
let buffer = match buffer_task.await {
|
||||
Ok(buffer) => buffer,
|
||||
Err(error) => {
|
||||
return this.update(cx, |this, cx| {
|
||||
this.error = Some(error);
|
||||
cx.notify();
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let buffers = futures::future::try_join_all(buffer_tasks).await?;
|
||||
let snapshot = buffer.update(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let query = SearchQuery::text(&excerpt.text_passage, false, false, false, vec![], vec![])?;
|
||||
let matches = query.search(&snapshot, None).await;
|
||||
let Some(first_match) = matches.first() else {
|
||||
log::warn!(
|
||||
"text {:?} does not appear in '{}'",
|
||||
excerpt.text_passage,
|
||||
excerpt.path
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let multibuffer = cx.new_model(|_cx| {
|
||||
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(title)
|
||||
})?;
|
||||
let editor =
|
||||
cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))?;
|
||||
this.update(cx, |this, cx| {
|
||||
let mut start = first_match.start.to_point(&snapshot);
|
||||
start.column = 0;
|
||||
|
||||
for (excerpt, buffer) in excerpts.iter().zip(buffers.iter()) {
|
||||
let snapshot = buffer.update(&mut cx, |buffer, _cx| buffer.snapshot())?;
|
||||
if let Some(editor) = &this.editor {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts_with_context_lines(
|
||||
buffer.clone(),
|
||||
vec![start..start],
|
||||
5,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(outline) = snapshot.outline(None) {
|
||||
let matches = outline
|
||||
.search(&excerpt.symbol_name, cx.background_executor().clone())
|
||||
.await;
|
||||
if let Some(mat) = matches.first() {
|
||||
let item = &outline.items[mat.candidate_id];
|
||||
let start = item.range.start.to_point(&snapshot);
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts_with_context_lines(
|
||||
buffer.clone(),
|
||||
vec![start..start],
|
||||
5,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let explanation = SharedString::from(excerpt.text.clone());
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
position: ranges[0].start,
|
||||
height: 2,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(move |cx| {
|
||||
Self::render_note_block(&explanation, cx)
|
||||
}),
|
||||
disposition: BlockDisposition::Above,
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
let annotation = SharedString::from(excerpt.annotation);
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
position: ranges[0].start,
|
||||
height: annotation.split('\n').count() as u8 + 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(move |cx| Self::render_note_block(&annotation, cx)),
|
||||
disposition: BlockDisposition::Above,
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
if !this.added_editor_to_workspace {
|
||||
this.added_editor_to_workspace = true;
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
anyhow::Ok("showed comments to users in a new view".into())
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| AnnotationResultView { output })
|
||||
}
|
||||
}
|
||||
|
||||
impl AnnotationTool {
|
||||
fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement {
|
||||
let anchor_x = cx.anchor_x;
|
||||
let gutter_width = cx.gutter_dimensions.width;
|
||||
@@ -179,24 +222,89 @@ impl AnnotationTool {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnnotationResultView {
|
||||
output: Result<String>,
|
||||
}
|
||||
|
||||
impl Render for AnnotationResultView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
match &self.output {
|
||||
Ok(output) => div().child(output.clone().into_any_element()),
|
||||
Err(error) => div().child(format!("failed to open path: {:?}", error)),
|
||||
if let Some(error) = &self.error {
|
||||
ui::Label::new(error.to_string()).into_any_element()
|
||||
} else {
|
||||
ui::Label::new(SharedString::from(format!(
|
||||
"Opened a buffer with {} excerpts",
|
||||
self.rendered_excerpt_count
|
||||
)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for AnnotationResultView {
|
||||
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
|
||||
match &self.output {
|
||||
Ok(output) => output.clone(),
|
||||
Err(err) => format!("Failed to create buffer: {err:?}"),
|
||||
type Input = AnnotationInput;
|
||||
type SerializedState = Option<String>;
|
||||
|
||||
fn generate(&self, _: &mut ProjectContext, _: &mut ViewContext<Self>) -> String {
|
||||
if let Some(error) = &self.error {
|
||||
format!("Failed to create buffer: {error:?}")
|
||||
} else {
|
||||
format!(
|
||||
"opened {} excerpts in a buffer",
|
||||
self.rendered_excerpt_count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_input(&mut self, mut input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
let editor = if let Some(editor) = &self.editor {
|
||||
editor.clone()
|
||||
} else {
|
||||
let multibuffer = cx.new_model(|_cx| {
|
||||
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(String::new())
|
||||
});
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), cx)
|
||||
});
|
||||
|
||||
self.editor = Some(editor.clone());
|
||||
editor
|
||||
};
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |multibuffer, cx| {
|
||||
if multibuffer.title(cx) != input.title {
|
||||
multibuffer.set_title(input.title.clone(), cx);
|
||||
}
|
||||
});
|
||||
|
||||
self.pending_excerpt = input.excerpts.pop();
|
||||
for excerpt in input.excerpts.iter().skip(self.rendered_excerpt_count) {
|
||||
self.tx.unbounded_send(excerpt.clone()).ok();
|
||||
}
|
||||
self.rendered_excerpt_count = input.excerpts.len();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
if let Some(excerpt) = self.pending_excerpt.take() {
|
||||
self.rendered_excerpt_count += 1;
|
||||
self.tx.unbounded_send(excerpt.clone()).ok();
|
||||
}
|
||||
|
||||
self.tx.close_channel();
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
self.error.as_ref().map(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some(error_message) = output {
|
||||
self.error = Some(anyhow::anyhow!("{}", error_message));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, Model, Task, View, WeakView};
|
||||
@@ -20,7 +20,7 @@ impl CreateBufferTool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Deserialize, JsonSchema)]
|
||||
pub struct CreateBufferInput {
|
||||
/// The contents of the buffer.
|
||||
text: String,
|
||||
@@ -32,25 +32,69 @@ pub struct CreateBufferInput {
|
||||
}
|
||||
|
||||
impl LanguageModelTool for CreateBufferTool {
|
||||
type Input = CreateBufferInput;
|
||||
type Output = ();
|
||||
type View = CreateBufferView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"create_buffer".to_string()
|
||||
"create_new_source_file".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Create a new buffer in the current codebase".to_string()
|
||||
"Create a new file in the current codebase. Only use this when generating new code, NOT when showing existing code from the project.".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| CreateBufferView {
|
||||
workspace: self.workspace.clone(),
|
||||
project: self.project.clone(),
|
||||
input: None,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateBufferView {
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
input: Option<CreateBufferInput>,
|
||||
error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
impl Render for CreateBufferView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().child("Opening a buffer")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for CreateBufferView {
|
||||
type Input = CreateBufferInput;
|
||||
|
||||
type SerializedState = ();
|
||||
|
||||
fn generate(&self, _project: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
|
||||
let Some(input) = self.input.as_ref() else {
|
||||
return "No input".to_string();
|
||||
};
|
||||
|
||||
match &self.error {
|
||||
None => format!("Created a new {} buffer", input.language),
|
||||
Some(err) => format!("Failed to create buffer: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, _cx: &mut ViewContext<Self>) {
|
||||
self.input = Some(input);
|
||||
}
|
||||
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
cx.spawn({
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let text = input.text.clone();
|
||||
let language_name = input.language.clone();
|
||||
|mut cx| async move {
|
||||
let input = self.input.clone();
|
||||
|_this, mut cx| async move {
|
||||
let input = input.ok_or_else(|| anyhow!("no input"))?;
|
||||
|
||||
let text = input.text.clone();
|
||||
let language_name = input.language.clone();
|
||||
let language = cx
|
||||
.update(|cx| {
|
||||
project
|
||||
@@ -86,34 +130,15 @@ impl LanguageModelTool for CreateBufferTool {
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| CreateBufferView {
|
||||
language: input.language,
|
||||
output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateBufferView {
|
||||
language: String,
|
||||
output: Result<()>,
|
||||
}
|
||||
|
||||
impl Render for CreateBufferView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().child("Opening a buffer")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for CreateBufferView {
|
||||
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
|
||||
match &self.output {
|
||||
Ok(_) => format!("Created a new {} buffer", self.language),
|
||||
Err(err) => format!("Failed to create buffer: {err:?}"),
|
||||
}
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
()
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
_output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ use gpui::{prelude::*, Model, Task};
|
||||
use project::ProjectPath;
|
||||
use schemars::JsonSchema;
|
||||
use semantic_index::{ProjectIndex, Status};
|
||||
use serde::Deserialize;
|
||||
use std::{fmt::Write as _, ops::Range};
|
||||
use ui::{div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write as _, ops::Range, path::Path, sync::Arc};
|
||||
use ui::{prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
|
||||
|
||||
const DEFAULT_SEARCH_LIMIT: usize = 20;
|
||||
|
||||
@@ -15,10 +15,26 @@ pub struct ProjectIndexTool {
|
||||
project_index: Model<ProjectIndex>,
|
||||
}
|
||||
|
||||
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
|
||||
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
|
||||
#[derive(Default)]
|
||||
enum ProjectIndexToolState {
|
||||
#[default]
|
||||
CollectingQuery,
|
||||
Searching,
|
||||
Error(anyhow::Error),
|
||||
Finished {
|
||||
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
|
||||
index_status: Status,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct ProjectIndexView {
|
||||
project_index: Model<ProjectIndex>,
|
||||
input: CodebaseQuery,
|
||||
expanded_header: bool,
|
||||
state: ProjectIndexToolState,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, JsonSchema)]
|
||||
pub struct CodebaseQuery {
|
||||
/// Semantic search query
|
||||
query: String,
|
||||
@@ -26,30 +42,19 @@ pub struct CodebaseQuery {
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct ProjectIndexView {
|
||||
input: CodebaseQuery,
|
||||
output: Result<ProjectIndexOutput>,
|
||||
element_id: ElementId,
|
||||
expanded_header: bool,
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SerializedState {
|
||||
index_status: Status,
|
||||
error_message: Option<String>,
|
||||
worktrees: BTreeMap<Arc<Path>, WorktreeIndexOutput>,
|
||||
}
|
||||
|
||||
pub struct ProjectIndexOutput {
|
||||
status: Status,
|
||||
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
struct WorktreeIndexOutput {
|
||||
excerpts: BTreeMap<Arc<Path>, Vec<Range<usize>>>,
|
||||
}
|
||||
|
||||
impl ProjectIndexView {
|
||||
fn new(input: CodebaseQuery, output: Result<ProjectIndexOutput>) -> Self {
|
||||
let element_id = ElementId::Name(nanoid::nanoid!().into());
|
||||
|
||||
Self {
|
||||
input,
|
||||
output,
|
||||
element_id,
|
||||
expanded_header: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.expanded_header = !self.expanded_header;
|
||||
cx.notify();
|
||||
@@ -60,81 +65,206 @@ impl Render for ProjectIndexView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let query = self.input.query.clone();
|
||||
|
||||
let result = &self.output;
|
||||
|
||||
let output = match result {
|
||||
Err(err) => {
|
||||
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
|
||||
let (header_text, content) = match &self.state {
|
||||
ProjectIndexToolState::Error(error) => {
|
||||
return format!("failed to search: {error:?}").into_any_element()
|
||||
}
|
||||
Ok(output) => output,
|
||||
};
|
||||
ProjectIndexToolState::CollectingQuery | ProjectIndexToolState::Searching => {
|
||||
("Searching...".to_string(), div())
|
||||
}
|
||||
ProjectIndexToolState::Finished { excerpts, .. } => {
|
||||
let file_count = excerpts.len();
|
||||
|
||||
let file_count = output.excerpts.len();
|
||||
let header_text = format!(
|
||||
"Read {} {}",
|
||||
file_count,
|
||||
if file_count == 1 { "file" } else { "files" }
|
||||
);
|
||||
|
||||
let el = v_flex().gap_2().children(excerpts.keys().map(|path| {
|
||||
h_flex().gap_2().child(Icon::new(IconName::File)).child(
|
||||
Label::new(path.path.to_string_lossy().to_string()).color(Color::Muted),
|
||||
)
|
||||
}));
|
||||
|
||||
(header_text, el)
|
||||
}
|
||||
};
|
||||
|
||||
let header = h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::File))
|
||||
.child(format!(
|
||||
"Read {} {}",
|
||||
file_count,
|
||||
if file_count == 1 { "file" } else { "files" }
|
||||
));
|
||||
.child(header_text);
|
||||
|
||||
v_flex().gap_3().child(
|
||||
CollapsibleContainer::new(self.element_id.clone(), self.expanded_header)
|
||||
.start_slot(header)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_header(cx);
|
||||
}))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.p_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.children(output.excerpts.keys().map(|path| {
|
||||
h_flex().gap_2().child(Icon::new(IconName::File)).child(
|
||||
Label::new(path.path.to_string_lossy().to_string())
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(
|
||||
CollapsibleContainer::new("collapsible-container", self.expanded_header)
|
||||
.start_slot(header)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_header(cx);
|
||||
}))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.p_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
|
||||
)
|
||||
.child(content),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for ProjectIndexView {
|
||||
type Input = CodebaseQuery;
|
||||
type SerializedState = SerializedState;
|
||||
|
||||
fn generate(
|
||||
&self,
|
||||
context: &mut assistant_tooling::ProjectContext,
|
||||
_: &mut WindowContext,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> String {
|
||||
match &self.output {
|
||||
Ok(output) => {
|
||||
match &self.state {
|
||||
ProjectIndexToolState::CollectingQuery => String::new(),
|
||||
ProjectIndexToolState::Searching => String::new(),
|
||||
ProjectIndexToolState::Error(error) => format!("failed to search: {error:?}"),
|
||||
ProjectIndexToolState::Finished {
|
||||
excerpts,
|
||||
index_status,
|
||||
} => {
|
||||
let mut body = "found results in the following paths:\n".to_string();
|
||||
|
||||
for (project_path, ranges) in &output.excerpts {
|
||||
for (project_path, ranges) in excerpts {
|
||||
context.add_excerpts(project_path.clone(), ranges);
|
||||
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
|
||||
}
|
||||
|
||||
if output.status != Status::Idle {
|
||||
if *index_status != Status::Idle {
|
||||
body.push_str("Still indexing. Results may be incomplete.\n");
|
||||
}
|
||||
|
||||
body
|
||||
}
|
||||
Err(err) => format!("Error: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
self.input = input;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
self.state = ProjectIndexToolState::Searching;
|
||||
cx.notify();
|
||||
|
||||
let project_index = self.project_index.read(cx);
|
||||
let index_status = project_index.status();
|
||||
let search = project_index.search(
|
||||
self.input.query.clone(),
|
||||
self.input.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let search_result = search.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
match search_result {
|
||||
Ok(search_results) => {
|
||||
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
|
||||
for search_result in search_results {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: search_result.worktree.read(cx).id(),
|
||||
path: search_result.path,
|
||||
};
|
||||
excerpts
|
||||
.entry(project_path)
|
||||
.or_default()
|
||||
.push(search_result.range);
|
||||
}
|
||||
this.state = ProjectIndexToolState::Finished {
|
||||
excerpts,
|
||||
index_status,
|
||||
};
|
||||
}
|
||||
Err(error) => {
|
||||
this.state = ProjectIndexToolState::Error(error);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
let mut serialized = SerializedState {
|
||||
error_message: None,
|
||||
index_status: Status::Idle,
|
||||
worktrees: Default::default(),
|
||||
};
|
||||
match &self.state {
|
||||
ProjectIndexToolState::Error(err) => serialized.error_message = Some(err.to_string()),
|
||||
ProjectIndexToolState::Finished {
|
||||
excerpts,
|
||||
index_status,
|
||||
} => {
|
||||
serialized.index_status = *index_status;
|
||||
if let Some(project) = self.project_index.read(cx).project().upgrade() {
|
||||
let project = project.read(cx);
|
||||
for (project_path, excerpts) in excerpts {
|
||||
if let Some(worktree) =
|
||||
project.worktree_for_id(project_path.worktree_id, cx)
|
||||
{
|
||||
let worktree_path = worktree.read(cx).abs_path();
|
||||
serialized
|
||||
.worktrees
|
||||
.entry(worktree_path)
|
||||
.or_default()
|
||||
.excerpts
|
||||
.insert(project_path.path.clone(), excerpts.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
serialized
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
serialized: Self::SerializedState,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
if !serialized.worktrees.is_empty() {
|
||||
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
|
||||
if let Some(project) = self.project_index.read(cx).project().upgrade() {
|
||||
let project = project.read(cx);
|
||||
for (worktree_path, worktree_state) in serialized.worktrees {
|
||||
if let Some(worktree) = project
|
||||
.worktrees()
|
||||
.find(|worktree| worktree.read(cx).abs_path() == worktree_path)
|
||||
{
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
for (path, serialized_excerpts) in worktree_state.excerpts {
|
||||
excerpts.insert(ProjectPath { worktree_id, path }, serialized_excerpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state = ProjectIndexToolState::Finished {
|
||||
excerpts,
|
||||
index_status: serialized.index_status,
|
||||
};
|
||||
}
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectIndexTool {
|
||||
@@ -144,8 +274,6 @@ impl ProjectIndexTool {
|
||||
}
|
||||
|
||||
impl LanguageModelTool for ProjectIndexTool {
|
||||
type Input = CodebaseQuery;
|
||||
type Output = ProjectIndexOutput;
|
||||
type View = ProjectIndexView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
@@ -156,54 +284,12 @@ impl LanguageModelTool for ProjectIndexTool {
|
||||
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of code chunks in the code base and an embedding of the query.".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, query: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
|
||||
let project_index = self.project_index.read(cx);
|
||||
let status = project_index.status();
|
||||
let search = project_index.search(
|
||||
query.query.clone(),
|
||||
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let search_results = search.await?;
|
||||
|
||||
cx.update(|cx| {
|
||||
let mut output = ProjectIndexOutput {
|
||||
status,
|
||||
excerpts: Default::default(),
|
||||
};
|
||||
|
||||
for search_result in search_results {
|
||||
let path = ProjectPath {
|
||||
worktree_id: search_result.worktree.read(cx).id(),
|
||||
path: search_result.path.clone(),
|
||||
};
|
||||
|
||||
let excerpts_for_path = output.excerpts.entry(path).or_default();
|
||||
let ix = match excerpts_for_path
|
||||
.binary_search_by_key(&search_result.range.start, |r| r.start)
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
excerpts_for_path.insert(ix, search_result.range);
|
||||
}
|
||||
|
||||
output
|
||||
})
|
||||
fn view(&self, cx: &mut WindowContext) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_| ProjectIndexView {
|
||||
state: ProjectIndexToolState::CollectingQuery,
|
||||
input: Default::default(),
|
||||
expanded_header: false,
|
||||
project_index: self.project_index.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| ProjectIndexView::new(input, output))
|
||||
}
|
||||
|
||||
fn render_running(_: &mut WindowContext) -> impl IntoElement {
|
||||
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false)
|
||||
.start_slot("Searching code base")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,68 +48,73 @@ impl RenderOnce for Composer {
|
||||
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let mut editor_border = cx.theme().colors().text;
|
||||
editor_border.fade_out(0.90);
|
||||
|
||||
// Remove the extra 1px added by the border
|
||||
let padding = Spacing::XLarge.rems(cx) - rems_from_px(1.);
|
||||
|
||||
h_flex()
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.w_full()
|
||||
.items_start()
|
||||
.child(
|
||||
v_flex().size_full().gap_1().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.p_3()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: line_height.into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
v_flex()
|
||||
.w_full()
|
||||
.rounded_lg()
|
||||
.p(padding)
|
||||
.border_1()
|
||||
.border_color(editor_border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: line_height.into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex().gap_1().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(self.render_tools(cx))
|
||||
.child(Divider::vertical())
|
||||
.child(self.render_attachment_tools(cx)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex().gap_1().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(self.render_tools(cx))
|
||||
.child(Divider::vertical())
|
||||
.child(self.render_attachment_tools(cx)),
|
||||
),
|
||||
)
|
||||
.child(h_flex().gap_1().child(self.model_selector)),
|
||||
),
|
||||
),
|
||||
),
|
||||
.child(h_flex().gap_1().child(self.model_selector)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,14 @@ anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
repair_json.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sum_tree.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -2,8 +2,12 @@ mod attachment_registry;
|
||||
mod project_context;
|
||||
mod tool_registry;
|
||||
|
||||
pub use attachment_registry::{AttachmentRegistry, LanguageModelAttachment, UserAttachment};
|
||||
pub use attachment_registry::{
|
||||
AttachmentOutput, AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment,
|
||||
UserAttachment,
|
||||
};
|
||||
pub use project_context::ProjectContext;
|
||||
pub use tool_registry::{
|
||||
LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition, ToolOutput, ToolRegistry,
|
||||
LanguageModelTool, SavedToolFunctionCall, ToolFunctionCall, ToolFunctionDefinition, ToolOutput,
|
||||
ToolRegistry,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::{ProjectContext, ToolOutput};
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use futures::future::join_all;
|
||||
use gpui::{AnyView, Render, Task, View, WindowContext};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
sync::{
|
||||
@@ -16,25 +18,39 @@ pub struct AttachmentRegistry {
|
||||
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
|
||||
}
|
||||
|
||||
pub trait AttachmentOutput {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
}
|
||||
|
||||
pub trait LanguageModelAttachment {
|
||||
type Output: 'static;
|
||||
type View: Render + ToolOutput;
|
||||
type Output: DeserializeOwned + Serialize + 'static;
|
||||
type View: Render + AttachmentOutput;
|
||||
|
||||
fn name(&self) -> Arc<str>;
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
|
||||
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
|
||||
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
/// A collected attachment from running an attachment tool
|
||||
pub struct UserAttachment {
|
||||
pub view: AnyView,
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedUserAttachment {
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
}
|
||||
|
||||
/// Internal representation of an attachment tool to allow us to treat them dynamically
|
||||
struct RegisteredAttachment {
|
||||
name: Arc<str>,
|
||||
enabled: AtomicBool,
|
||||
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
|
||||
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
|
||||
}
|
||||
|
||||
impl AttachmentRegistry {
|
||||
@@ -45,24 +61,65 @@ impl AttachmentRegistry {
|
||||
}
|
||||
|
||||
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
|
||||
let call = Box::new(move |cx: &mut WindowContext| {
|
||||
let result = attachment.run(cx);
|
||||
let attachment = Arc::new(attachment);
|
||||
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<A::Output> = result.await;
|
||||
let view = cx.update(|cx| A::view(result, cx))?;
|
||||
let call = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
let result = attachment.run(cx);
|
||||
let attachment = attachment.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<A::Output> = result.await;
|
||||
let serialized_output =
|
||||
result
|
||||
.as_ref()
|
||||
.map_err(ToString::to_string)
|
||||
.and_then(|output| {
|
||||
Ok(RawValue::from_string(
|
||||
serde_json::to_string(output).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.unwrap())
|
||||
});
|
||||
|
||||
let view = cx.update(|cx| attachment.view(result, cx))?;
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: attachment.name(),
|
||||
view: view.into(),
|
||||
generate_fn: generate::<A>,
|
||||
serialized_output,
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let deserialize = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
|
||||
let serialized_output = saved_attachment.serialized_output.clone();
|
||||
let output = match &serialized_output {
|
||||
Ok(serialized_output) => {
|
||||
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
|
||||
}
|
||||
Err(error) => Err(anyhow!("{error}")),
|
||||
};
|
||||
let view = attachment.view(output, cx).into();
|
||||
|
||||
Ok(UserAttachment {
|
||||
view: view.into(),
|
||||
name: saved_attachment.name.clone(),
|
||||
view,
|
||||
serialized_output,
|
||||
generate_fn: generate::<A>,
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
self.registered_attachments.insert(
|
||||
TypeId::of::<A>(),
|
||||
RegisteredAttachment {
|
||||
name: attachment.name(),
|
||||
call,
|
||||
deserialize,
|
||||
enabled: AtomicBool::new(true),
|
||||
},
|
||||
);
|
||||
@@ -134,6 +191,35 @@ impl AttachmentRegistry {
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_user_attachment(
|
||||
&self,
|
||||
user_attachment: &UserAttachment,
|
||||
) -> SavedUserAttachment {
|
||||
SavedUserAttachment {
|
||||
name: user_attachment.name.clone(),
|
||||
serialized_output: user_attachment.serialized_output.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_user_attachment(
|
||||
&self,
|
||||
saved_user_attachment: SavedUserAttachment,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<UserAttachment> {
|
||||
if let Some(registered_attachment) = self
|
||||
.registered_attachments
|
||||
.values()
|
||||
.find(|attachment| attachment.name == saved_user_attachment.name)
|
||||
{
|
||||
(registered_attachment.deserialize)(&saved_user_attachment, cx)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"no attachment tool for name {}",
|
||||
saved_user_attachment.name
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAttachment {
|
||||
|
||||
@@ -1,41 +1,67 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{
|
||||
div, AnyElement, AnyView, IntoElement, ParentElement, Render, Styled, Task, View, WindowContext,
|
||||
};
|
||||
use gpui::{AnyElement, AnyView, IntoElement, Render, Task, View, WindowContext};
|
||||
use repair_json::repair;
|
||||
use schemars::{schema::RootSchema, schema_for, JsonSchema};
|
||||
use serde::Deserialize;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
mem,
|
||||
sync::atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
|
||||
use crate::ProjectContext;
|
||||
use ui::ViewContext;
|
||||
|
||||
pub struct ToolRegistry {
|
||||
registered_tools: HashMap<String, RegisteredTool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[derive(Default)]
|
||||
pub struct ToolFunctionCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
#[serde(skip)]
|
||||
pub result: Option<ToolFunctionCallResult>,
|
||||
state: ToolFunctionCallState,
|
||||
}
|
||||
|
||||
pub enum ToolFunctionCallResult {
|
||||
#[derive(Default)]
|
||||
enum ToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
ParsingFailed,
|
||||
Finished {
|
||||
view: AnyView,
|
||||
generate_fn: fn(AnyView, &mut ProjectContext, &mut WindowContext) -> String,
|
||||
},
|
||||
KnownTool(Box<dyn ToolView>),
|
||||
ExecutedTool(Box<dyn ToolView>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
trait ToolView {
|
||||
fn view(&self) -> AnyView;
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext);
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>>;
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>>;
|
||||
fn deserialize_output(&self, raw_value: &RawValue, cx: &mut WindowContext) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct SavedToolFunctionCall {
|
||||
id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
state: SavedToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
enum SavedToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool,
|
||||
ExecutedTool(Box<RawValue>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ToolFunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
@@ -43,14 +69,7 @@ pub struct ToolFunctionDefinition {
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: for<'de> Deserialize<'de> + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type Output: 'static;
|
||||
|
||||
type View: Render + ToolOutput;
|
||||
type View: ToolOutput;
|
||||
|
||||
/// Returns the name of the tool.
|
||||
///
|
||||
@@ -66,7 +85,7 @@ pub trait LanguageModelTool {
|
||||
|
||||
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
|
||||
fn definition(&self) -> ToolFunctionDefinition {
|
||||
let root_schema = schema_for!(Self::Input);
|
||||
let root_schema = schema_for!(<Self::View as ToolOutput>::Input);
|
||||
|
||||
ToolFunctionDefinition {
|
||||
name: self.name(),
|
||||
@@ -75,29 +94,34 @@ pub trait LanguageModelTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the tool with the given input.
|
||||
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
|
||||
fn output_view(
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View>;
|
||||
|
||||
fn render_running(_cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
}
|
||||
/// A view of the output of running the tool, for displaying to the user.
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
pub trait ToolOutput: Sized {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
pub trait ToolOutput: Render {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: DeserializeOwned + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type SerializedState: DeserializeOwned + Serialize;
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut ViewContext<Self>) -> String;
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>);
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
|
||||
|
||||
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState;
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
struct RegisteredTool {
|
||||
enabled: AtomicBool,
|
||||
type_id: TypeId,
|
||||
call: Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
|
||||
render_running: fn(&mut WindowContext) -> gpui::AnyElement,
|
||||
build_view: Box<dyn Fn(&mut WindowContext) -> Box<dyn ToolView>>,
|
||||
definition: ToolFunctionDefinition,
|
||||
}
|
||||
|
||||
@@ -134,68 +158,141 @@ impl ToolRegistry {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn render_tool_call(
|
||||
pub fn update_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
call: &mut ToolFunctionCall,
|
||||
name: Option<&str>,
|
||||
arguments: Option<&str>,
|
||||
cx: &mut WindowContext,
|
||||
) -> AnyElement {
|
||||
match &tool_call.result {
|
||||
Some(result) => div()
|
||||
.p_2()
|
||||
.child(result.into_any_element(&tool_call.name))
|
||||
.into_any_element(),
|
||||
None => self
|
||||
.registered_tools
|
||||
.get(&tool_call.name)
|
||||
.map(|tool| (tool.render_running)(cx))
|
||||
.unwrap_or_else(|| div().into_any_element()),
|
||||
) {
|
||||
if let Some(name) = name {
|
||||
call.name.push_str(name);
|
||||
}
|
||||
if let Some(arguments) = arguments {
|
||||
if call.arguments.is_empty() {
|
||||
if let Some(tool) = self.registered_tools.get(&call.name) {
|
||||
let view = (tool.build_view)(cx);
|
||||
call.state = ToolFunctionCallState::KnownTool(view);
|
||||
} else {
|
||||
call.state = ToolFunctionCallState::NoSuchTool;
|
||||
}
|
||||
}
|
||||
call.arguments.push_str(arguments);
|
||||
|
||||
if let ToolFunctionCallState::KnownTool(view) = &call.state {
|
||||
if let Ok(repaired_arguments) = repair(call.arguments.clone()) {
|
||||
view.try_set_input(&repaired_arguments, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(
|
||||
&mut self,
|
||||
tool: T,
|
||||
pub fn execute_tool_call(
|
||||
&self,
|
||||
tool_call: &mut ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if let ToolFunctionCallState::KnownTool(view) = mem::take(&mut tool_call.state) {
|
||||
let task = view.execute(cx);
|
||||
tool_call.state = ToolFunctionCallState::ExecutedTool(view);
|
||||
Some(task)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
) -> Option<AnyElement> {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
Some(ui::Label::new("No such tool").into_any_element())
|
||||
}
|
||||
ToolFunctionCallState::Initializing => None,
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
Some(view.view().into_any_element())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_for_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
project_context: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::Initializing => String::new(),
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
format!("No such tool: {}", tool_call.name)
|
||||
}
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
view.generate(project_context, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_tool_call(
|
||||
&self,
|
||||
call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<SavedToolFunctionCall> {
|
||||
Ok(SavedToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
ToolFunctionCallState::Initializing => SavedToolFunctionCallState::Initializing,
|
||||
ToolFunctionCallState::NoSuchTool => SavedToolFunctionCallState::NoSuchTool,
|
||||
ToolFunctionCallState::KnownTool(_) => SavedToolFunctionCallState::KnownTool,
|
||||
ToolFunctionCallState::ExecutedTool(view) => {
|
||||
SavedToolFunctionCallState::ExecutedTool(view.serialize_output(cx)?)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize_tool_call(
|
||||
&self,
|
||||
call: &SavedToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<ToolFunctionCall> {
|
||||
let Some(tool) = self.registered_tools.get(&call.name) else {
|
||||
return Err(anyhow!("no such tool {}", call.name));
|
||||
};
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
SavedToolFunctionCallState::Initializing => ToolFunctionCallState::Initializing,
|
||||
SavedToolFunctionCallState::NoSuchTool => ToolFunctionCallState::NoSuchTool,
|
||||
SavedToolFunctionCallState::KnownTool => {
|
||||
log::error!("Deserialized tool that had not executed");
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
ToolFunctionCallState::KnownTool(view)
|
||||
}
|
||||
SavedToolFunctionCallState::ExecutedTool(output) => {
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
view.deserialize_output(output, cx)?;
|
||||
ToolFunctionCallState::ExecutedTool(view)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
|
||||
let name = tool.name();
|
||||
let registered_tool = RegisteredTool {
|
||||
type_id: TypeId::of::<T>(),
|
||||
definition: tool.definition(),
|
||||
enabled: AtomicBool::new(true),
|
||||
call: Box::new(
|
||||
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
|
||||
let name = tool_call.name.clone();
|
||||
let arguments = tool_call.arguments.clone();
|
||||
let id = tool_call.id.clone();
|
||||
|
||||
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
|
||||
return Task::ready(Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::ParsingFailed),
|
||||
}));
|
||||
};
|
||||
|
||||
let result = tool.execute(&input, cx);
|
||||
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<T::Output> = result.await;
|
||||
let view = cx.update(|cx| T::output_view(input, result, cx))?;
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::Finished {
|
||||
view: view.into(),
|
||||
generate_fn: generate::<T>,
|
||||
}),
|
||||
})
|
||||
})
|
||||
},
|
||||
),
|
||||
render_running: render_running::<T>,
|
||||
build_view: Box::new(move |cx: &mut WindowContext| Box::new(tool.view(cx))),
|
||||
};
|
||||
|
||||
let previous = self.registered_tools.insert(name.clone(), registered_tool);
|
||||
@@ -204,77 +301,40 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
|
||||
fn render_running<T: LanguageModelTool>(cx: &mut WindowContext) -> AnyElement {
|
||||
T::render_running(cx).into_any_element()
|
||||
}
|
||||
|
||||
fn generate<T: LanguageModelTool>(
|
||||
view: AnyView,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
view.downcast::<T::View>()
|
||||
.unwrap()
|
||||
.update(cx, |view, cx| T::View::generate(view, project, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Task yields an error if the window for the given WindowContext is closed before the task completes.
|
||||
pub fn call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<ToolFunctionCall>> {
|
||||
let name = tool_call.name.clone();
|
||||
let arguments = tool_call.arguments.clone();
|
||||
let id = tool_call.id.clone();
|
||||
|
||||
let tool = match self.registered_tools.get(&name) {
|
||||
Some(tool) => tool,
|
||||
None => {
|
||||
let name = name.clone();
|
||||
return Task::ready(Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::NoSuchTool),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
(tool.call)(tool_call, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFunctionCallResult {
|
||||
pub fn generate(
|
||||
&self,
|
||||
name: &String,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
match self {
|
||||
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
|
||||
ToolFunctionCallResult::ParsingFailed => {
|
||||
format!("Unable to parse arguments for {name}")
|
||||
}
|
||||
ToolFunctionCallResult::Finished { generate_fn, view } => {
|
||||
(generate_fn)(view.clone(), project, cx)
|
||||
}
|
||||
impl<T: ToolOutput> ToolView for View<T> {
|
||||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
|
||||
self.update(cx, |view, cx| view.generate(project, cx))
|
||||
}
|
||||
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext) {
|
||||
if let Ok(input) = serde_json::from_str::<T::Input>(input) {
|
||||
self.update(cx, |view, cx| {
|
||||
view.set_input(input, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn into_any_element(&self, name: &String) -> AnyElement {
|
||||
match self {
|
||||
ToolFunctionCallResult::NoSuchTool => {
|
||||
format!("Language Model attempted to call {name}").into_any_element()
|
||||
}
|
||||
ToolFunctionCallResult::ParsingFailed => {
|
||||
format!("Language Model called {name} with bad arguments").into_any_element()
|
||||
}
|
||||
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
|
||||
}
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>> {
|
||||
self.update(cx, |view, cx| view.execute(cx))
|
||||
}
|
||||
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>> {
|
||||
let output = self.update(cx, |view, cx| view.serialize(cx));
|
||||
Ok(RawValue::from_string(serde_json::to_string(&output)?)?)
|
||||
}
|
||||
|
||||
fn deserialize_output(&self, output: &RawValue, cx: &mut WindowContext) -> Result<()> {
|
||||
let state = serde_json::from_str::<T::SerializedState>(output.get())?;
|
||||
self.update(cx, |view, cx| view.deserialize(state, cx))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +353,6 @@ mod test {
|
||||
use super::*;
|
||||
use gpui::{div, prelude::*, Render, TestAppContext};
|
||||
use gpui::{EmptyView, View};
|
||||
use schemars::schema_for;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@@ -304,10 +363,6 @@ mod test {
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct WeatherResult {
|
||||
location: String,
|
||||
@@ -316,24 +371,81 @@ mod test {
|
||||
}
|
||||
|
||||
struct WeatherView {
|
||||
result: WeatherResult,
|
||||
input: Option<WeatherQuery>,
|
||||
result: Option<WeatherResult>,
|
||||
|
||||
// Fake API call
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
impl WeatherView {
|
||||
fn new(current_weather: WeatherResult) -> Self {
|
||||
Self {
|
||||
input: None,
|
||||
result: None,
|
||||
current_weather,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for WeatherView {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
div().child(format!("temperature: {}", self.result.temperature))
|
||||
match self.result {
|
||||
Some(ref result) => div()
|
||||
.child(format!("temperature: {}", result.temperature))
|
||||
.into_any_element(),
|
||||
None => div().child("Calculating weather...").into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for WeatherView {
|
||||
fn generate(&self, _output: &mut ProjectContext, _cx: &mut WindowContext) -> String {
|
||||
type Input = WeatherQuery;
|
||||
|
||||
type SerializedState = WeatherResult;
|
||||
|
||||
fn generate(&self, _output: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
|
||||
serde_json::to_string(&self.result).unwrap()
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
self.input = Some(input);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
let input = self.input.as_ref().unwrap();
|
||||
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
self.result = Some(weather);
|
||||
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
self.current_weather.clone()
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
self.current_weather = output;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WeatherTool {
|
||||
type Input = WeatherQuery;
|
||||
type Output = WeatherResult;
|
||||
type View = WeatherView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
@@ -344,88 +456,71 @@ mod test {
|
||||
"Fetches the current weather for a given location.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
Task::ready(Ok(weather))
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| {
|
||||
let result = result.unwrap();
|
||||
WeatherView { result }
|
||||
})
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| WeatherView::new(self.current_weather.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
cx.background_executor.run_until_parked();
|
||||
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
|
||||
|
||||
let tool = WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let tools = vec![tool.definition()];
|
||||
assert_eq!(tools.len(), 1);
|
||||
|
||||
let expected = ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: schema_for!(WeatherQuery),
|
||||
};
|
||||
|
||||
assert_eq!(tools[0].name, expected.name);
|
||||
assert_eq!(tools[0].description, expected.description);
|
||||
|
||||
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_schema,
|
||||
json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry
|
||||
.register(WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let definitions = registry.definitions();
|
||||
assert_eq!(
|
||||
definitions,
|
||||
[ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: serde_json::from_value(json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}))
|
||||
.unwrap(),
|
||||
}]
|
||||
);
|
||||
|
||||
let args = json!({
|
||||
"location": "San Francisco",
|
||||
"unit": "Celsius"
|
||||
let mut call = ToolFunctionCall {
|
||||
id: "the-id".to_string(),
|
||||
name: "get_cur".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let task = cx.update(|cx| {
|
||||
registry.update_tool_call(
|
||||
&mut call,
|
||||
Some("rent_weather"),
|
||||
Some(r#"{"location": "San Francisco","#),
|
||||
cx,
|
||||
);
|
||||
registry.update_tool_call(&mut call, None, Some(r#" "unit": "Celsius"}"#), cx);
|
||||
registry.execute_tool_call(&mut call, cx).unwrap()
|
||||
});
|
||||
task.await.unwrap();
|
||||
|
||||
let query: WeatherQuery = serde_json::from_value(args).unwrap();
|
||||
|
||||
let result = cx.update(|cx| tool.execute(&query, cx)).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result, tool.current_weather);
|
||||
match &call.state {
|
||||
ToolFunctionCallState::ExecutedTool(_view) => {}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,16 +56,22 @@ struct UpdateRequestBody {
|
||||
telemetry: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum AutoUpdateStatus {
|
||||
Idle,
|
||||
Checking,
|
||||
Downloading,
|
||||
Installing,
|
||||
Updated,
|
||||
Updated { binary_path: PathBuf },
|
||||
Errored,
|
||||
}
|
||||
|
||||
impl AutoUpdateStatus {
|
||||
pub fn is_updated(&self) -> bool {
|
||||
matches!(self, Self::Updated { .. })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AutoUpdater {
|
||||
status: AutoUpdateStatus,
|
||||
current_version: SemanticVersion,
|
||||
@@ -306,7 +312,7 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
|
||||
if self.pending_poll.is_some() || self.status.is_updated() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -328,7 +334,7 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
pub fn status(&self) -> AutoUpdateStatus {
|
||||
self.status
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
|
||||
@@ -404,6 +410,11 @@ impl AutoUpdater {
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
// We store the path of our current binary, before we install, since installation might
|
||||
// delete it. Once deleted, it's hard to get the path to our binary on Linux.
|
||||
// So we cache it here, which allows us to then restart later on.
|
||||
let binary_path = cx.update(|cx| cx.app_path())??;
|
||||
|
||||
match OS {
|
||||
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
|
||||
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
|
||||
@@ -413,7 +424,7 @@ impl AutoUpdater {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.status = AutoUpdateStatus::Updated;
|
||||
this.status = AutoUpdateStatus::Updated { binary_path };
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
|
||||
@@ -19,11 +19,17 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
libc.workspace = true
|
||||
ipc-channel = "0.18"
|
||||
once_cell.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
exec.workspace = true
|
||||
fork.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation.workspace = true
|
||||
core-services = "0.2"
|
||||
|
||||
@@ -13,6 +13,7 @@ pub enum CliRequest {
|
||||
paths: Vec<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
dev_server_token: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
#![cfg_attr(any(target_os = "linux", target_os = "windows"), allow(dead_code))]
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use cli::{CliRequest, CliResponse};
|
||||
use serde::Deserialize;
|
||||
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use util::paths::PathLikeWithPosition;
|
||||
|
||||
struct Detect;
|
||||
|
||||
trait InstalledApp {
|
||||
fn zed_version_string(&self) -> String;
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "zed", disable_version_flag = true)]
|
||||
struct Args {
|
||||
@@ -33,9 +37,9 @@ struct Args {
|
||||
/// Print Zed's version and the app path.
|
||||
#[arg(short, long)]
|
||||
version: bool,
|
||||
/// Custom Zed.app path
|
||||
#[arg(short, long)]
|
||||
bundle_path: Option<PathBuf>,
|
||||
/// Custom path to Zed.app or the zed binary
|
||||
#[arg(long)]
|
||||
zed: Option<PathBuf>,
|
||||
/// Run zed in dev-server mode
|
||||
#[arg(long)]
|
||||
dev_server_token: Option<String>,
|
||||
@@ -49,12 +53,6 @@ fn parse_path_with_position(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
bundle_short_version_string: String,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Intercept version designators
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -68,14 +66,10 @@ fn main() -> Result<()> {
|
||||
}
|
||||
let args = Args::parse();
|
||||
|
||||
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
|
||||
|
||||
if let Some(dev_server_token) = args.dev_server_token {
|
||||
return bundle.spawn(vec!["--dev-server-token".into(), dev_server_token]);
|
||||
}
|
||||
let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
|
||||
|
||||
if args.version {
|
||||
println!("{}", bundle.zed_version_string());
|
||||
println!("{}", app.zed_version_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -101,7 +95,14 @@ fn main() -> Result<()> {
|
||||
paths.push(canonicalized.to_string(|path| path.display().to_string()))
|
||||
}
|
||||
|
||||
let (tx, rx) = bundle.launch()?;
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
app.launch(url)?;
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
|
||||
let open_new_workspace = if args.new {
|
||||
Some(true)
|
||||
} else if args.add {
|
||||
@@ -114,6 +115,7 @@ fn main() -> Result<()> {
|
||||
paths,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
})?;
|
||||
|
||||
while let Ok(response) = rx.recv() {
|
||||
@@ -128,60 +130,125 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Bundle {
|
||||
App {
|
||||
app_bundle: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
LocalPath {
|
||||
executable: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
}
|
||||
|
||||
fn locate_bundle() -> Result<PathBuf> {
|
||||
let cli_path = std::env::current_exe()?.canonicalize()?;
|
||||
let mut app_path = cli_path.clone();
|
||||
while app_path.extension() != Some(OsStr::new("app")) {
|
||||
if !app_path.pop() {
|
||||
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
|
||||
}
|
||||
}
|
||||
Ok(app_path)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux {
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsString,
|
||||
io,
|
||||
os::{
|
||||
linux::net::SocketAddrExt,
|
||||
unix::net::{SocketAddr, UnixDatagram},
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
process, thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use cli::{CliRequest, CliResponse};
|
||||
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
||||
use anyhow::anyhow;
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
use fork::Fork;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{Bundle, InfoPlist};
|
||||
use crate::{Detect, InstalledApp};
|
||||
|
||||
impl Bundle {
|
||||
pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||
unimplemented!()
|
||||
static RELEASE_CHANNEL: Lazy<String> =
|
||||
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
|
||||
|
||||
struct App(PathBuf);
|
||||
|
||||
impl Detect {
|
||||
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
|
||||
let path = if let Some(path) = path {
|
||||
path.to_path_buf().canonicalize()
|
||||
} else {
|
||||
let cli = env::current_exe()?;
|
||||
let dir = cli
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("no parent path for cli"))?;
|
||||
|
||||
match dir.join("zed").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
// development builds have Zed capitalized
|
||||
Err(e) => match dir.join("Zed").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_) => Err(e),
|
||||
},
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(App(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl InstalledApp for App {
|
||||
fn zed_version_string(&self) -> String {
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
if *RELEASE_CHANNEL == "stable" {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(" {} ", *RELEASE_CHANNEL)
|
||||
},
|
||||
option_env!("RELEASE_VERSION").unwrap_or_default(),
|
||||
self.0.display(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn plist(&self) -> &InfoPlist {
|
||||
unimplemented!()
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
let uid: u32 = unsafe { libc::getuid() };
|
||||
let sock_addr =
|
||||
SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
|
||||
|
||||
let sock = UnixDatagram::unbound()?;
|
||||
if sock.connect_addr(&sock_addr).is_err() {
|
||||
self.boot_background(ipc_url)?;
|
||||
} else {
|
||||
sock.send(ipc_url.as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
let path = &self.0;
|
||||
|
||||
match fork::fork() {
|
||||
Ok(Fork::Parent(_)) => Ok(()),
|
||||
Ok(Fork::Child) => {
|
||||
std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
|
||||
if let Err(_) = fork::setsid() {
|
||||
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
|
||||
process::exit(1);
|
||||
}
|
||||
if std::env::var("ZED_KEEP_FD").is_err() {
|
||||
if let Err(_) = fork::close_fd() {
|
||||
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
let error =
|
||||
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
|
||||
// if exec succeeded, we never get here.
|
||||
eprintln!("failed to exec {:?}: {}", path, error);
|
||||
process::exit(1)
|
||||
}
|
||||
Err(_) => Err(anyhow!(io::Error::last_os_error())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn zed_version_string(&self) -> String {
|
||||
unimplemented!()
|
||||
fn wait_for_socket(
|
||||
&self,
|
||||
sock_addr: &SocketAddr,
|
||||
sock: &mut UnixDatagram,
|
||||
) -> Result<(), std::io::Error> {
|
||||
for _ in 0..100 {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
if sock.connect_addr(&sock_addr).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
sock.connect_addr(&sock_addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,59 +256,79 @@ mod linux {
|
||||
// todo("windows")
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows {
|
||||
use crate::{Detect, InstalledApp};
|
||||
use std::path::Path;
|
||||
|
||||
use cli::{CliRequest, CliResponse};
|
||||
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
||||
|
||||
use crate::{Bundle, InfoPlist};
|
||||
|
||||
impl Bundle {
|
||||
pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||
struct App;
|
||||
impl InstalledApp for App {
|
||||
fn zed_version_string(&self) -> String {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn plist(&self) -> &InfoPlist {
|
||||
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn zed_version_string(&self) -> String {
|
||||
unimplemented!()
|
||||
impl Detect {
|
||||
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
|
||||
Ok(App)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_os {
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use core_foundation::{
|
||||
array::{CFArray, CFIndex},
|
||||
string::kCFStringEncodingUTF8,
|
||||
url::{CFURLCreateWithBytes, CFURL},
|
||||
};
|
||||
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
|
||||
use std::{fs, path::Path, process::Command, ptr};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
ptr,
|
||||
};
|
||||
|
||||
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
|
||||
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
|
||||
use crate::{locate_bundle, Bundle, InfoPlist};
|
||||
use crate::{Detect, InstalledApp};
|
||||
|
||||
impl Bundle {
|
||||
pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||
let bundle_path = if let Some(bundle_path) = args_bundle_path {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
bundle_short_version_string: String,
|
||||
}
|
||||
|
||||
enum Bundle {
|
||||
App {
|
||||
app_bundle: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
LocalPath {
|
||||
executable: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
}
|
||||
|
||||
fn locate_bundle() -> Result<PathBuf> {
|
||||
let cli_path = std::env::current_exe()?.canonicalize()?;
|
||||
let mut app_path = cli_path.clone();
|
||||
while app_path.extension() != Some(OsStr::new("app")) {
|
||||
if !app_path.pop() {
|
||||
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
|
||||
}
|
||||
}
|
||||
Ok(app_path)
|
||||
}
|
||||
|
||||
impl Detect {
|
||||
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
|
||||
let bundle_path = if let Some(bundle_path) = path {
|
||||
bundle_path
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
|
||||
@@ -256,7 +343,7 @@ mod mac_os {
|
||||
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
|
||||
format!("Reading *.app bundle plist file at {plist_path:?}")
|
||||
})?;
|
||||
Ok(Self::App {
|
||||
Ok(Bundle::App {
|
||||
app_bundle: bundle_path,
|
||||
plist,
|
||||
})
|
||||
@@ -271,42 +358,27 @@ mod mac_os {
|
||||
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
|
||||
format!("Reading dev bundle plist file at {plist_path:?}")
|
||||
})?;
|
||||
Ok(Self::LocalPath {
|
||||
Ok(Bundle::LocalPath {
|
||||
executable: bundle_path,
|
||||
plist,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plist(&self) -> &InfoPlist {
|
||||
match self {
|
||||
Self::App { plist, .. } => plist,
|
||||
Self::LocalPath { plist, .. } => plist,
|
||||
}
|
||||
impl InstalledApp for Bundle {
|
||||
fn zed_version_string(&self) -> String {
|
||||
let is_dev = matches!(self, Self::LocalPath { .. });
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
self.plist().bundle_short_version_string,
|
||||
if is_dev { " (dev)" } else { "" },
|
||||
self.path().display(),
|
||||
)
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => app_bundle,
|
||||
Self::LocalPath { executable, .. } => executable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(&self, args: Vec<String>) -> Result<()> {
|
||||
let path = match self {
|
||||
Self::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
|
||||
Self::LocalPath { executable, .. } => executable.clone(),
|
||||
};
|
||||
Command::new(path).args(args).status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
fn launch(&self, url: String) -> anyhow::Result<()> {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => {
|
||||
let app_path = app_bundle;
|
||||
@@ -368,18 +440,23 @@ mod mac_os {
|
||||
}
|
||||
}
|
||||
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
Ok((handshake.requests, handshake.responses))
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
fn plist(&self) -> &InfoPlist {
|
||||
match self {
|
||||
Self::App { plist, .. } => plist,
|
||||
Self::LocalPath { plist, .. } => plist,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zed_version_string(&self) -> String {
|
||||
let is_dev = matches!(self, Self::LocalPath { .. });
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
self.plist().bundle_short_version_string,
|
||||
if is_dev { " (dev)" } else { "" },
|
||||
self.path().display(),
|
||||
)
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => app_bundle,
|
||||
Self::LocalPath { executable, .. } => executable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ futures.workspace = true
|
||||
gpui.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
once_cell = "1.19.0"
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
|
||||
@@ -415,7 +415,7 @@ impl Database {
|
||||
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
|
||||
let base_delay = SLEEPS[prev_attempt_count];
|
||||
let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0);
|
||||
log::info!(
|
||||
log::warn!(
|
||||
"retrying transaction after serialization error. delay: {} ms.",
|
||||
randomized_delay
|
||||
);
|
||||
|
||||
@@ -130,13 +130,21 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_project(&self, project_id: ProjectId) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
project::Entity::delete_by_id(project_id).exec(&*tx).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Unshares the given project.
|
||||
pub async fn unshare_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
user_id: Option<UserId>,
|
||||
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||
) -> Result<TransactionGuard<(bool, Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||
self.project_transaction(project_id, |tx| async move {
|
||||
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
@@ -149,10 +157,7 @@ impl Database {
|
||||
None
|
||||
};
|
||||
if project.host_connection()? == connection {
|
||||
project::Entity::delete(project.into_active_model())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
return Ok((room, guest_connection_ids));
|
||||
return Ok((true, room, guest_connection_ids));
|
||||
}
|
||||
if let Some(dev_server_project_id) = project.dev_server_project_id {
|
||||
if let Some(user_id) = user_id {
|
||||
@@ -169,7 +174,7 @@ impl Database {
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
return Ok((room, guest_connection_ids));
|
||||
return Ok((false, room, guest_connection_ids));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2032,23 +2032,34 @@ async fn unshare_project_internal(
|
||||
user_id: Option<UserId>,
|
||||
session: &Session,
|
||||
) -> Result<()> {
|
||||
let (room, guest_connection_ids) = &*session
|
||||
.db()
|
||||
.await
|
||||
.unshare_project(project_id, connection_id, user_id)
|
||||
.await?;
|
||||
let delete = {
|
||||
let room_guard = session
|
||||
.db()
|
||||
.await
|
||||
.unshare_project(project_id, connection_id, user_id)
|
||||
.await?;
|
||||
|
||||
let message = proto::UnshareProject {
|
||||
project_id: project_id.to_proto(),
|
||||
let (delete, room, guest_connection_ids) = &*room_guard;
|
||||
|
||||
let message = proto::UnshareProject {
|
||||
project_id: project_id.to_proto(),
|
||||
};
|
||||
|
||||
broadcast(
|
||||
Some(connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|conn_id| session.peer.send(conn_id, message.clone()),
|
||||
);
|
||||
if let Some(room) = room {
|
||||
room_updated(room, &session.peer);
|
||||
}
|
||||
|
||||
*delete
|
||||
};
|
||||
|
||||
broadcast(
|
||||
Some(connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|conn_id| session.peer.send(conn_id, message.clone()),
|
||||
);
|
||||
if let Some(room) = room {
|
||||
room_updated(room, &session.peer);
|
||||
if delete {
|
||||
let db = session.db().await;
|
||||
db.delete_project(project_id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -428,8 +428,10 @@ impl TestServer {
|
||||
node_runtime: app_state.node_runtime.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
TestClient {
|
||||
app_state,
|
||||
|
||||
@@ -677,7 +677,7 @@ impl CollabTitlebarItem {
|
||||
client::Status::UpgradeRequired => {
|
||||
let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Installing)
|
||||
| Some(AutoUpdateStatus::Downloading)
|
||||
| Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
@@ -691,7 +691,7 @@ impl CollabTitlebarItem {
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(|_, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
if auto_updater.read(cx).status().is_updated() {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ impl CollabNotification {
|
||||
}
|
||||
|
||||
impl ParentElement for CollabNotification {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,11 +429,17 @@ impl Copilot {
|
||||
env: None,
|
||||
};
|
||||
|
||||
let root_path = if cfg!(target_os = "windows") {
|
||||
Path::new("C:/")
|
||||
} else {
|
||||
Path::new("/")
|
||||
};
|
||||
|
||||
let server = LanguageServer::new(
|
||||
Arc::new(Mutex::new(None)),
|
||||
new_server_id,
|
||||
binary,
|
||||
Path::new("/"),
|
||||
root_path,
|
||||
None,
|
||||
cx.clone(),
|
||||
)?;
|
||||
|
||||
@@ -52,16 +52,11 @@ pub struct SelectToEndOfLine {
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct ToggleCodeActions {
|
||||
// Display row from which the action was deployed.
|
||||
#[serde(default)]
|
||||
pub deployed_from_indicator: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct ToggleTestRunner {
|
||||
#[serde(default)]
|
||||
pub deployed_from_row: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct ConfirmCompletion {
|
||||
#[serde(default)]
|
||||
|
||||
@@ -477,6 +477,11 @@ impl DisplaySnapshot {
|
||||
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
|
||||
}
|
||||
|
||||
pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
|
||||
self.buffer_snapshot
|
||||
.anchor_at(point.to_offset(&self, bias), bias)
|
||||
}
|
||||
|
||||
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
@@ -721,6 +726,10 @@ impl DisplaySnapshot {
|
||||
DisplayPoint(clipped)
|
||||
}
|
||||
|
||||
pub fn clip_ignoring_line_ends(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
||||
DisplayPoint(self.block_snapshot.clip_point(point.0, bias))
|
||||
}
|
||||
|
||||
pub fn clip_at_line_end(&self, point: DisplayPoint) -> DisplayPoint {
|
||||
let mut point = point.0;
|
||||
if point.column == self.line_len(point.row) {
|
||||
|
||||
@@ -79,7 +79,6 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion_provider::*;
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::Runnable;
|
||||
use language::{
|
||||
char_kind,
|
||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||
@@ -87,7 +86,8 @@ use language::{
|
||||
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||
Point, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
use task::{ResolvedTask, TaskTemplate};
|
||||
use language::{Runnable, RunnableRange};
|
||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||
|
||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||
@@ -404,6 +404,7 @@ struct RunnableTasks {
|
||||
templates: Vec<(TaskSourceKind, TaskTemplate)>,
|
||||
// We need the column at which the task context evaluation should take place.
|
||||
column: u32,
|
||||
extra_variables: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -504,7 +505,7 @@ pub struct Editor {
|
||||
>,
|
||||
last_bounds: Option<Bounds<Pixels>>,
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: HashMap<u32, RunnableTasks>,
|
||||
tasks: HashMap<(BufferId, u32), (usize, RunnableTasks)>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
@@ -3839,7 +3840,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
drop(context_menu);
|
||||
|
||||
let snapshot = self.snapshot(cx);
|
||||
let deployed_from_indicator = action.deployed_from_indicator;
|
||||
let mut task = self.code_actions_task.take();
|
||||
let action = action.clone();
|
||||
@@ -3851,11 +3852,24 @@ impl Editor {
|
||||
|
||||
let spawned_test_task = this.update(&mut cx, |this, cx| {
|
||||
if this.focus_handle.is_focused(cx) {
|
||||
let buffer_row = action
|
||||
let display_row = action
|
||||
.deployed_from_indicator
|
||||
.map(|row| {
|
||||
DisplayPoint::new(row, 0)
|
||||
.to_point(&snapshot.display_snapshot)
|
||||
.row
|
||||
})
|
||||
.unwrap_or_else(|| this.selections.newest::<Point>(cx).head().row);
|
||||
let tasks = this.tasks.get(&buffer_row).map(|t| Arc::new(t.to_owned()));
|
||||
let (location, code_actions) = this
|
||||
let (buffer, buffer_row) = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_line_for_row(display_row)
|
||||
.and_then(|(buffer_snapshot, range)| {
|
||||
this.buffer
|
||||
.read(cx)
|
||||
.buffer(buffer_snapshot.remote_id())
|
||||
.map(|buffer| (buffer, range.start.row))
|
||||
})?;
|
||||
let (_, code_actions) = this
|
||||
.available_code_actions
|
||||
.clone()
|
||||
.and_then(|(location, code_actions)| {
|
||||
@@ -3869,25 +3883,20 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
.unzip();
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let tasks = this
|
||||
.tasks
|
||||
.get(&(buffer_id, buffer_row))
|
||||
.map(|t| Arc::new(t.to_owned()));
|
||||
if tasks.is_none() && code_actions.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let buffer = location.map(|location| location.buffer).or_else(|| {
|
||||
let snapshot = this.snapshot(cx);
|
||||
let (buffer_snapshot, _) =
|
||||
snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
this.buffer().read(cx).buffer(buffer_id)
|
||||
});
|
||||
let Some(buffer) = buffer else {
|
||||
return None;
|
||||
};
|
||||
this.completion_tasks.clear();
|
||||
this.discard_inline_completion(cx);
|
||||
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|
||||
|(tasks, (workspace, _))| {
|
||||
let position = Point::new(buffer_row, tasks.column);
|
||||
let position = Point::new(buffer_row, tasks.1.column);
|
||||
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
|
||||
let location = Location {
|
||||
buffer: buffer.clone(),
|
||||
@@ -3901,22 +3910,33 @@ impl Editor {
|
||||
.flatten()
|
||||
},
|
||||
);
|
||||
let tasks = tasks
|
||||
.zip(task_context.as_ref())
|
||||
.map(|(tasks, task_context)| {
|
||||
Arc::new(ResolvedTasks {
|
||||
templates: tasks
|
||||
.templates
|
||||
.iter()
|
||||
.filter_map(|(kind, template)| {
|
||||
template
|
||||
.resolve_task(&kind.to_id_base(), &task_context)
|
||||
.map(|task| (kind.clone(), task))
|
||||
})
|
||||
.collect(),
|
||||
position: Point::new(buffer_row, tasks.column),
|
||||
})
|
||||
});
|
||||
let tasks = tasks.zip(task_context).map(|(tasks, mut task_context)| {
|
||||
// Fill in the environmental variables from the tree-sitter captures
|
||||
let mut additional_task_variables = TaskVariables::default();
|
||||
for (capture_name, value) in tasks.1.extra_variables.clone() {
|
||||
additional_task_variables.insert(
|
||||
task::VariableName::Custom(capture_name.into()),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
task_context
|
||||
.task_variables
|
||||
.extend(additional_task_variables);
|
||||
|
||||
Arc::new(ResolvedTasks {
|
||||
templates: tasks
|
||||
.1
|
||||
.templates
|
||||
.iter()
|
||||
.filter_map(|(kind, template)| {
|
||||
template
|
||||
.resolve_task(&kind.to_id_base(), &task_context)
|
||||
.map(|task| (kind.clone(), task))
|
||||
})
|
||||
.collect(),
|
||||
position: Point::new(buffer_row, tasks.1.column),
|
||||
})
|
||||
});
|
||||
let spawn_straight_away = tasks
|
||||
.as_ref()
|
||||
.map_or(false, |tasks| tasks.templates.len() == 1)
|
||||
@@ -4505,8 +4525,8 @@ impl Editor {
|
||||
self.tasks.clear()
|
||||
}
|
||||
|
||||
fn insert_tasks(&mut self, row: u32, tasks: RunnableTasks) {
|
||||
if let Some(_) = self.tasks.insert(row, tasks) {
|
||||
fn insert_tasks(&mut self, key: (BufferId, u32), value: (usize, RunnableTasks)) {
|
||||
if let Some(_) = self.tasks.insert(key, value) {
|
||||
// This case should hopefully be rare, but just in case...
|
||||
log::error!("multiple different run targets found on a single line, only the last target will be rendered")
|
||||
}
|
||||
@@ -4639,11 +4659,12 @@ impl Editor {
|
||||
delta +=
|
||||
snippet.text.len() as isize - insertion_range.len() as isize;
|
||||
|
||||
let start = ((insertion_start + tabstop_range.start) as usize)
|
||||
.min(snapshot.len());
|
||||
let end = ((insertion_start + tabstop_range.end) as usize)
|
||||
.min(snapshot.len());
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
||||
let start = snapshot.anchor_before(
|
||||
(insertion_start + tabstop_range.start) as usize,
|
||||
);
|
||||
let end = snapshot
|
||||
.anchor_after((insertion_start + tabstop_range.end) as usize);
|
||||
start..end
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -7725,8 +7746,8 @@ impl Editor {
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.clear_tasks();
|
||||
for (row, tasks) in rows {
|
||||
this.insert_tasks(row, tasks);
|
||||
for (key, value) in rows {
|
||||
this.insert_tasks(key, value);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
@@ -7735,32 +7756,47 @@ impl Editor {
|
||||
fn fetch_runnable_ranges(
|
||||
snapshot: &DisplaySnapshot,
|
||||
range: Range<Anchor>,
|
||||
) -> Vec<(Range<usize>, Runnable)> {
|
||||
) -> Vec<language::RunnableRange> {
|
||||
snapshot.buffer_snapshot.runnable_ranges(range).collect()
|
||||
}
|
||||
|
||||
fn runnable_rows(
|
||||
project: Model<Project>,
|
||||
snapshot: DisplaySnapshot,
|
||||
runnable_ranges: Vec<(Range<usize>, Runnable)>,
|
||||
runnable_ranges: Vec<RunnableRange>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Vec<(u32, RunnableTasks)> {
|
||||
) -> Vec<((BufferId, u32), (usize, RunnableTasks))> {
|
||||
runnable_ranges
|
||||
.into_iter()
|
||||
.filter_map(|(multi_buffer_range, mut runnable)| {
|
||||
.filter_map(|mut runnable| {
|
||||
let (tasks, _) = cx
|
||||
.update(|cx| Self::resolve_runnable(project.clone(), &mut runnable, cx))
|
||||
.update(|cx| {
|
||||
Self::resolve_runnable(project.clone(), &mut runnable.runnable, cx)
|
||||
})
|
||||
.ok()?;
|
||||
if tasks.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let point = multi_buffer_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
let row = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_line_for_row(point.row)?
|
||||
.1
|
||||
.start
|
||||
.row;
|
||||
|
||||
Some((
|
||||
point.row,
|
||||
RunnableTasks {
|
||||
templates: tasks,
|
||||
column: point.column,
|
||||
},
|
||||
(runnable.buffer_id, row),
|
||||
(
|
||||
runnable.run_range.start,
|
||||
RunnableTasks {
|
||||
templates: tasks,
|
||||
column: point.column,
|
||||
extra_variables: runnable.extra_captures,
|
||||
},
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
@@ -9206,6 +9242,8 @@ impl Editor {
|
||||
self.active_diagnostics = Some(active_diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9239,6 +9277,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10114,6 +10153,7 @@ impl Editor {
|
||||
predecessor,
|
||||
excerpts,
|
||||
} => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(cx));
|
||||
cx.emit(EditorEvent::ExcerptsAdded {
|
||||
buffer: buffer.clone(),
|
||||
predecessor: *predecessor,
|
||||
|
||||
@@ -1406,17 +1406,24 @@ impl EditorElement {
|
||||
};
|
||||
editor
|
||||
.tasks
|
||||
.keys()
|
||||
.map(|row| {
|
||||
.iter()
|
||||
.filter_map(|((_, row), (multibuffer_offset, _))| {
|
||||
if snapshot.is_line_folded(*row) {
|
||||
return None;
|
||||
}
|
||||
let display_row = snapshot
|
||||
.buffer_snapshot
|
||||
.offset_to_point(*multibuffer_offset)
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row();
|
||||
|
||||
let button = editor.render_run_indicator(
|
||||
&self.style,
|
||||
Some(*row) == active_task_indicator_row,
|
||||
*row,
|
||||
display_row,
|
||||
cx,
|
||||
);
|
||||
let display_row = Point::new(*row, 0)
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row();
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
display_row,
|
||||
@@ -1426,7 +1433,7 @@ impl EditorElement {
|
||||
gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
button
|
||||
Some(button)
|
||||
})
|
||||
.collect_vec()
|
||||
})
|
||||
@@ -2859,7 +2866,6 @@ impl EditorElement {
|
||||
let snapshot = layout.position_map.snapshot.clone();
|
||||
let theme = cx.theme().clone();
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
let max_row = layout.max_row;
|
||||
|
||||
editor.scrollbar_marker_state.dirty = false;
|
||||
editor.scrollbar_marker_state.pending_refresh =
|
||||
@@ -2868,12 +2874,12 @@ impl EditorElement {
|
||||
let scrollbar_markers = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let max_point = snapshot.display_snapshot.buffer_snapshot.max_point();
|
||||
let mut marker_quads = Vec::new();
|
||||
|
||||
if scrollbar_settings.git_diff {
|
||||
let marker_row_ranges = snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..max_row)
|
||||
.git_diff_hunks_in_range(0..max_point.row)
|
||||
.map(|hunk| {
|
||||
let start_display_row =
|
||||
Point::new(hunk.associated_range.start, 0)
|
||||
@@ -2942,9 +2948,6 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if scrollbar_settings.diagnostics {
|
||||
let max_point =
|
||||
snapshot.display_snapshot.buffer_snapshot.max_point();
|
||||
|
||||
let diagnostics = snapshot
|
||||
.buffer_snapshot
|
||||
.diagnostics_in_range::<_, Point>(
|
||||
@@ -3661,7 +3664,7 @@ impl Element for EditorElement {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
|
||||
cx.request_layout(&style, None)
|
||||
cx.request_layout(style, None)
|
||||
}
|
||||
EditorMode::AutoHeight { max_lines } => {
|
||||
let editor_handle = cx.view().clone();
|
||||
@@ -3685,7 +3688,7 @@ impl Element for EditorElement {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = relative(1.).into();
|
||||
cx.request_layout(&style, None)
|
||||
cx.request_layout(style, None)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4032,20 +4035,25 @@ impl Element for EditorElement {
|
||||
if gutter_settings.code_actions {
|
||||
let newest_selection_point =
|
||||
newest_selection_head.to_point(&snapshot.display_snapshot);
|
||||
let has_test_indicator = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.tasks
|
||||
.contains_key(&newest_selection_point.row);
|
||||
if !has_test_indicator {
|
||||
code_actions_indicator = self.layout_code_actions_indicator(
|
||||
line_height,
|
||||
newest_selection_head,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
let buffer = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_line_for_row(newest_selection_point.row);
|
||||
if let Some((buffer, range)) = buffer {
|
||||
let buffer_id = buffer.remote_id();
|
||||
let row = range.start.row;
|
||||
let has_test_indicator =
|
||||
self.editor.read(cx).tasks.contains_key(&(buffer_id, row));
|
||||
|
||||
if !has_test_indicator {
|
||||
code_actions_indicator = self.layout_code_actions_indicator(
|
||||
line_height,
|
||||
newest_selection_head,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4145,7 +4153,6 @@ impl Element for EditorElement {
|
||||
gutter_dimensions,
|
||||
content_origin,
|
||||
scrollbar_layout,
|
||||
max_row,
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
@@ -4278,7 +4285,6 @@ pub struct EditorLayout {
|
||||
cursors: Vec<(DisplayPoint, Hsla)>,
|
||||
visible_cursors: Vec<CursorLayout>,
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
max_row: u32,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
fold_indicators: Vec<Option<AnyElement>>,
|
||||
@@ -4495,20 +4501,26 @@ fn layout_line(
|
||||
) -> Result<ShapedLine> {
|
||||
let mut line = snapshot.line(row);
|
||||
|
||||
if line.len() > MAX_LINE_LEN {
|
||||
let mut len = MAX_LINE_LEN;
|
||||
while !line.is_char_boundary(len) {
|
||||
len -= 1;
|
||||
}
|
||||
let len = {
|
||||
let line_len = line.len();
|
||||
if line_len > MAX_LINE_LEN {
|
||||
let mut len = MAX_LINE_LEN;
|
||||
while !line.is_char_boundary(len) {
|
||||
len -= 1;
|
||||
}
|
||||
|
||||
line.truncate(len);
|
||||
}
|
||||
line.truncate(len);
|
||||
len
|
||||
} else {
|
||||
line_len
|
||||
}
|
||||
};
|
||||
|
||||
cx.text_system().shape_line(
|
||||
line.into(),
|
||||
style.text.font_size.to_pixels(cx.rem_size()),
|
||||
&[TextRun {
|
||||
len: snapshot.line_len(row) as usize,
|
||||
len,
|
||||
font: style.text.font(),
|
||||
color: Hsla::default(),
|
||||
background_color: None,
|
||||
|
||||
@@ -6,7 +6,7 @@ use anyhow::Context;
|
||||
use gpui::WindowContext;
|
||||
use language::{BasicContextProvider, ContextProvider};
|
||||
use project::{Location, WorktreeId};
|
||||
use task::{TaskContext, TaskVariables};
|
||||
use task::{TaskContext, TaskVariables, VariableName};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -79,7 +79,21 @@ pub(crate) fn task_context_with_editor(
|
||||
buffer,
|
||||
range: start..end,
|
||||
};
|
||||
task_context_for_location(workspace, location, cx)
|
||||
task_context_for_location(workspace, location.clone(), cx).map(|mut task_context| {
|
||||
for range in location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.runnable_ranges(location.range)
|
||||
{
|
||||
for (capture_name, value) in range.extra_captures {
|
||||
task_context
|
||||
.task_variables
|
||||
.insert(VariableName::Custom(capture_name.into()), value);
|
||||
}
|
||||
}
|
||||
task_context
|
||||
})
|
||||
}
|
||||
|
||||
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
|
||||
|
||||
@@ -164,7 +164,7 @@ pub struct ExtensionIndexLanguageEntry {
|
||||
actions!(zed, [ReloadExtensions]);
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<fs::RealFs>,
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<Client>,
|
||||
node_runtime: Arc<dyn NodeRuntime>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
|
||||
@@ -23,7 +23,7 @@ impl ExtensionCard {
|
||||
}
|
||||
|
||||
impl ParentElement for ExtensionCard {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ impl PickerDelegate for NewPathDelegate {
|
||||
if exists {
|
||||
self.should_dismiss = false;
|
||||
let answer = cx.prompt(
|
||||
gpui::PromptLevel::Destructive,
|
||||
gpui::PromptLevel::Critical,
|
||||
&format!("{} already exists. Do you want to replace it?", m.relative_path()),
|
||||
Some(
|
||||
"A file or folder with the same name already eixsts. Replacing it will overwrite its current contents.",
|
||||
|
||||
@@ -29,7 +29,7 @@ git.workspace = true
|
||||
git2.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
libc = "0.2"
|
||||
libc.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
gpui = { workspace = true, optional = true }
|
||||
|
||||
@@ -642,8 +642,8 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Restart the application.
|
||||
pub fn restart(&self) {
|
||||
self.platform.restart()
|
||||
pub fn restart(&self, binary_path: Option<PathBuf>) {
|
||||
self.platform.restart(binary_path)
|
||||
}
|
||||
|
||||
/// Returns the local timezone at the platform level.
|
||||
|
||||
@@ -140,7 +140,7 @@ pub trait RenderOnce: 'static {
|
||||
/// can accept any number of any kind of child elements
|
||||
pub trait ParentElement {
|
||||
/// Extend this element's children with the given child elements.
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>);
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>);
|
||||
|
||||
/// Add a single child element to this element.
|
||||
fn child(mut self, child: impl IntoElement) -> Self
|
||||
@@ -603,7 +603,7 @@ impl Element for Empty {
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
(cx.request_layout(&Style::default(), None), ())
|
||||
(cx.request_layout(Style::default(), None), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Anchored {
|
||||
}
|
||||
|
||||
impl ParentElement for Anchored {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ impl Element for Anchored {
|
||||
..Style::default()
|
||||
};
|
||||
|
||||
let layout_id = cx.request_layout(&anchored_style, child_layout_ids.iter().copied());
|
||||
let layout_id = cx.request_layout(anchored_style, child_layout_ids.iter().copied());
|
||||
|
||||
(layout_id, AnchoredState { child_layout_ids })
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl<T: 'static> Element for Canvas<T> {
|
||||
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let layout_id = cx.request_layout(&style, []);
|
||||
let layout_id = cx.request_layout(style.clone(), []);
|
||||
(layout_id, style)
|
||||
}
|
||||
|
||||
|
||||
@@ -1139,7 +1139,7 @@ impl Element for Div {
|
||||
.iter_mut()
|
||||
.map(|child| child.request_layout(cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
cx.request_layout(&style, child_layout_ids.iter().copied())
|
||||
cx.request_layout(style, child_layout_ids.iter().copied())
|
||||
})
|
||||
});
|
||||
(layout_id, DivFrameState { child_layout_ids })
|
||||
@@ -2337,7 +2337,7 @@ impl<E> ParentElement for Focusable<E>
|
||||
where
|
||||
E: ParentElement,
|
||||
{
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.element.extend(elements)
|
||||
}
|
||||
}
|
||||
@@ -2430,7 +2430,7 @@ impl<E> ParentElement for Stateful<E>
|
||||
where
|
||||
E: ParentElement,
|
||||
{
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.element.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ impl Element for Img {
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(&style, [])
|
||||
cx.request_layout(style, [])
|
||||
});
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
@@ -766,7 +766,7 @@ impl Element for List {
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
cx.request_layout(&style, None)
|
||||
cx.request_layout(style, None)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Element for Svg {
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |style, cx| cx.request_layout(&style, None));
|
||||
.request_layout(global_id, cx, |style, cx| cx.request_layout(style, None));
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::{
|
||||
use util::ResultExt;
|
||||
|
||||
impl Element for &'static str {
|
||||
type RequestLayoutState = TextState;
|
||||
type RequestLayoutState = TextLayout;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -29,7 +29,7 @@ impl Element for &'static str {
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let mut state = TextLayout::default();
|
||||
let layout_id = state.layout(SharedString::from(*self), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
@@ -37,21 +37,22 @@ impl Element for &'static str {
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_text_state: &mut Self::RequestLayoutState,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_layout: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
) {
|
||||
text_layout.prepaint(bounds, self)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut TextState,
|
||||
_bounds: Bounds<Pixels>,
|
||||
text_layout: &mut TextLayout,
|
||||
_: &mut (),
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
text_state.paint(bounds, self, cx)
|
||||
text_layout.paint(self, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +73,7 @@ impl IntoElement for String {
|
||||
}
|
||||
|
||||
impl Element for SharedString {
|
||||
type RequestLayoutState = TextState;
|
||||
type RequestLayoutState = TextLayout;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -86,7 +87,7 @@ impl Element for SharedString {
|
||||
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let mut state = TextLayout::default();
|
||||
let layout_id = state.layout(self.clone(), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
@@ -94,22 +95,22 @@ impl Element for SharedString {
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_text_state: &mut Self::RequestLayoutState,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_layout: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
) {
|
||||
text_layout.prepaint(bounds, self.as_ref())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
_bounds: Bounds<Pixels>,
|
||||
text_layout: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let text_str: &str = self.as_ref();
|
||||
text_state.paint(bounds, text_str, cx)
|
||||
text_layout.paint(self.as_ref(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +130,7 @@ impl IntoElement for SharedString {
|
||||
pub struct StyledText {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
layout: TextLayout,
|
||||
}
|
||||
|
||||
impl StyledText {
|
||||
@@ -137,9 +139,15 @@ impl StyledText {
|
||||
StyledText {
|
||||
text: text.into(),
|
||||
runs: None,
|
||||
layout: TextLayout::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn layout(&self) -> &TextLayout {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
/// Set the styling attributes for the given text, as well as
|
||||
/// as any ranges of text that have had their style customized.
|
||||
pub fn with_highlights(
|
||||
@@ -167,10 +175,16 @@ impl StyledText {
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text runs for this piece of text.
|
||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for StyledText {
|
||||
type RequestLayoutState = TextState;
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -184,29 +198,29 @@ impl Element for StyledText {
|
||||
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
|
||||
(layout_id, state)
|
||||
let layout_id = self.layout.layout(self.text.clone(), self.runs.take(), cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::RequestLayoutState,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
) {
|
||||
self.layout.prepaint(bounds, &self.text)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
text_state.paint(bounds, &self.text, cx)
|
||||
self.layout.paint(&self.text, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,19 +232,20 @@ impl IntoElement for StyledText {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// todo!()
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||
pub struct TextLayout(Arc<Mutex<Option<TextLayoutInner>>>);
|
||||
|
||||
struct TextStateInner {
|
||||
struct TextLayoutInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
size: Option<Size<Pixels>>,
|
||||
bounds: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
impl TextState {
|
||||
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
|
||||
impl TextLayout {
|
||||
fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
|
||||
self.0.lock()
|
||||
}
|
||||
|
||||
@@ -265,11 +280,11 @@ impl TextState {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(text_state) = element_state.0.lock().as_ref() {
|
||||
if text_state.size.is_some()
|
||||
&& (wrap_width.is_none() || wrap_width == text_state.wrap_width)
|
||||
if let Some(text_layout) = element_state.0.lock().as_ref() {
|
||||
if text_layout.size.is_some()
|
||||
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
|
||||
{
|
||||
return text_state.size.unwrap();
|
||||
return text_layout.size.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,11 +298,12 @@ impl TextState {
|
||||
)
|
||||
.log_err()
|
||||
else {
|
||||
element_state.lock().replace(TextStateInner {
|
||||
element_state.lock().replace(TextLayoutInner {
|
||||
lines: Default::default(),
|
||||
line_height,
|
||||
wrap_width,
|
||||
size: Some(Size::default()),
|
||||
bounds: None,
|
||||
});
|
||||
return Size::default();
|
||||
};
|
||||
@@ -299,11 +315,12 @@ impl TextState {
|
||||
size.width = size.width.max(line_size.width).ceil();
|
||||
}
|
||||
|
||||
element_state.lock().replace(TextStateInner {
|
||||
element_state.lock().replace(TextLayoutInner {
|
||||
lines,
|
||||
line_height,
|
||||
wrap_width,
|
||||
size: Some(size),
|
||||
bounds: None,
|
||||
});
|
||||
|
||||
size
|
||||
@@ -313,12 +330,25 @@ impl TextState {
|
||||
layout_id
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
|
||||
fn prepaint(&mut self, bounds: Bounds<Pixels>, text: &str) {
|
||||
let mut element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||
.unwrap();
|
||||
element_state.bounds = Some(bounds);
|
||||
}
|
||||
|
||||
fn paint(&mut self, text: &str, cx: &mut WindowContext) {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||
.unwrap();
|
||||
let bounds = element_state
|
||||
.bounds
|
||||
.ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text))
|
||||
.unwrap();
|
||||
|
||||
let line_height = element_state.line_height;
|
||||
let mut line_origin = bounds.origin;
|
||||
@@ -328,15 +358,19 @@ impl TextState {
|
||||
}
|
||||
}
|
||||
|
||||
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||
if !bounds.contains(&position) {
|
||||
return None;
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.expect("measurement has not been performed");
|
||||
let bounds = element_state
|
||||
.bounds
|
||||
.expect("prepaint has not been performed");
|
||||
|
||||
if position.y < bounds.top() {
|
||||
return Err(0);
|
||||
}
|
||||
|
||||
let line_height = element_state.line_height;
|
||||
let mut line_origin = bounds.origin;
|
||||
@@ -348,14 +382,56 @@ impl TextState {
|
||||
line_start_ix += line.len() + 1;
|
||||
} else {
|
||||
let position_within_line = position - line_origin;
|
||||
let index_within_line =
|
||||
line.index_for_position(position_within_line, line_height)?;
|
||||
return Some(line_start_ix + index_within_line);
|
||||
match line.index_for_position(position_within_line, line_height) {
|
||||
Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
|
||||
Err(index_within_line) => return Err(line_start_ix + index_within_line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(line_start_ix.saturating_sub(1))
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.expect("measurement has not been performed");
|
||||
let bounds = element_state
|
||||
.bounds
|
||||
.expect("prepaint has not been performed");
|
||||
let line_height = element_state.line_height;
|
||||
|
||||
let mut line_origin = bounds.origin;
|
||||
let mut line_start_ix = 0;
|
||||
|
||||
for line in &element_state.lines {
|
||||
let line_end_ix = line_start_ix + line.len();
|
||||
if index < line_start_ix {
|
||||
break;
|
||||
} else if index > line_end_ix {
|
||||
line_origin.y += line.size(line_height).height;
|
||||
line_start_ix = line_end_ix + 1;
|
||||
continue;
|
||||
} else {
|
||||
let ix_within_line = index - line_start_ix;
|
||||
return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.0.lock().as_ref().unwrap().bounds.unwrap()
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn line_height(&self) -> Pixels {
|
||||
self.0.lock().as_ref().unwrap().line_height
|
||||
}
|
||||
}
|
||||
|
||||
/// A text element that can be interacted with.
|
||||
@@ -436,7 +512,7 @@ impl InteractiveText {
|
||||
}
|
||||
|
||||
impl Element for InteractiveText {
|
||||
type RequestLayoutState = TextState;
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -484,17 +560,18 @@ impl Element for InteractiveText {
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Hitbox,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let text_layout = self.text.layout().clone();
|
||||
cx.with_element_state::<InteractiveTextState, _>(
|
||||
global_id.unwrap(),
|
||||
|interactive_state, cx| {
|
||||
let mut interactive_state = interactive_state.unwrap_or_default();
|
||||
if let Some(click_listener) = self.click_listener.take() {
|
||||
let mouse_position = cx.mouse_position();
|
||||
if let Some(ix) = text_state.index_for_position(bounds, mouse_position) {
|
||||
if let Some(ix) = text_layout.index_for_position(mouse_position).ok() {
|
||||
if self
|
||||
.clickable_ranges
|
||||
.iter()
|
||||
@@ -504,7 +581,7 @@ impl Element for InteractiveText {
|
||||
}
|
||||
}
|
||||
|
||||
let text_state = text_state.clone();
|
||||
let text_layout = text_layout.clone();
|
||||
let mouse_down = interactive_state.mouse_down_index.clone();
|
||||
if let Some(mouse_down_index) = mouse_down.get() {
|
||||
let hitbox = hitbox.clone();
|
||||
@@ -512,7 +589,7 @@ impl Element for InteractiveText {
|
||||
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||
if let Some(mouse_up_index) =
|
||||
text_state.index_for_position(bounds, event.position)
|
||||
text_layout.index_for_position(event.position).ok()
|
||||
{
|
||||
click_listener(
|
||||
&clickable_ranges,
|
||||
@@ -533,7 +610,7 @@ impl Element for InteractiveText {
|
||||
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||
if let Some(mouse_down_index) =
|
||||
text_state.index_for_position(bounds, event.position)
|
||||
text_layout.index_for_position(event.position).ok()
|
||||
{
|
||||
mouse_down.set(Some(mouse_down_index));
|
||||
cx.refresh();
|
||||
@@ -546,12 +623,12 @@ impl Element for InteractiveText {
|
||||
cx.on_mouse_event({
|
||||
let mut hover_listener = self.hover_listener.take();
|
||||
let hitbox = hitbox.clone();
|
||||
let text_state = text_state.clone();
|
||||
let text_layout = text_layout.clone();
|
||||
let hovered_index = interactive_state.hovered_index.clone();
|
||||
move |event: &MouseMoveEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||
let current = hovered_index.get();
|
||||
let updated = text_state.index_for_position(bounds, event.position);
|
||||
let updated = text_layout.index_for_position(event.position).ok();
|
||||
if current != updated {
|
||||
hovered_index.set(updated);
|
||||
if let Some(hover_listener) = hover_listener.as_ref() {
|
||||
@@ -567,10 +644,10 @@ impl Element for InteractiveText {
|
||||
let hitbox = hitbox.clone();
|
||||
let active_tooltip = interactive_state.active_tooltip.clone();
|
||||
let pending_mouse_down = interactive_state.mouse_down_index.clone();
|
||||
let text_state = text_state.clone();
|
||||
let text_layout = text_layout.clone();
|
||||
|
||||
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||
let position = text_state.index_for_position(bounds, event.position);
|
||||
let position = text_layout.index_for_position(event.position).ok();
|
||||
let is_hovered = position.is_some()
|
||||
&& hitbox.is_hovered(cx)
|
||||
&& pending_mouse_down.get().is_none();
|
||||
@@ -621,7 +698,7 @@ impl Element for InteractiveText {
|
||||
});
|
||||
}
|
||||
|
||||
self.text.paint(None, bounds, text_state, &mut (), cx);
|
||||
self.text.paint(None, bounds, &mut (), &mut (), cx);
|
||||
|
||||
((), interactive_state)
|
||||
},
|
||||
|
||||
@@ -98,7 +98,7 @@ pub(crate) trait Platform: 'static {
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
|
||||
fn quit(&self);
|
||||
fn restart(&self);
|
||||
fn restart(&self, binary_path: Option<PathBuf>);
|
||||
fn activate(&self, ignoring_other_apps: bool);
|
||||
fn hide(&self);
|
||||
fn hide_other_apps(&self);
|
||||
@@ -719,10 +719,6 @@ pub enum PromptLevel {
|
||||
|
||||
/// A prompt that is shown when a critical problem has occurred
|
||||
Critical,
|
||||
|
||||
/// A prompt that is shown when asking the user to confirm a potentially destructive action
|
||||
/// (overwriting a file for example)
|
||||
Destructive,
|
||||
}
|
||||
|
||||
/// The style of the cursor (pointer)
|
||||
|
||||
@@ -162,7 +162,7 @@ impl BladeAtlasState {
|
||||
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||
}
|
||||
AtlasTextureKind::Polychrome => {
|
||||
format = gpu::TextureFormat::Bgra8Unorm;
|
||||
format = gpu::TextureFormat::Bgra8UnormSrgb;
|
||||
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||
}
|
||||
AtlasTextureKind::Path => {
|
||||
|
||||
@@ -360,9 +360,7 @@ impl BladeRenderer {
|
||||
size: config.size,
|
||||
usage: gpu::TextureUsage::TARGET,
|
||||
display_sync: gpu::DisplaySync::Recent,
|
||||
//Note: this matches the original logic of the Metal backend,
|
||||
// but ultimaterly we need to switch to `Linear`.
|
||||
color_space: gpu::ColorSpace::Srgb,
|
||||
color_space: gpu::ColorSpace::Linear,
|
||||
allow_exclusive_full_screen: false,
|
||||
transparent: config.transparent,
|
||||
};
|
||||
|
||||
@@ -88,6 +88,14 @@ fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds:
|
||||
return distance_from_clip_rect_impl(position, clip_bounds);
|
||||
}
|
||||
|
||||
// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
|
||||
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = srgb < vec3<f32>(0.04045);
|
||||
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
|
||||
let lower = srgb / vec3<f32>(12.92);
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
|
||||
let s = hsla.s;
|
||||
@@ -97,8 +105,7 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
|
||||
let x = c * (1.0 - abs(h % 2.0 - 1.0));
|
||||
let m = l - c / 2.0;
|
||||
|
||||
var color = vec4<f32>(m, m, m, a);
|
||||
var color = vec3<f32>(m);
|
||||
|
||||
if (h >= 0.0 && h < 1.0) {
|
||||
color.r += c;
|
||||
@@ -120,7 +127,12 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
color.b += x;
|
||||
}
|
||||
|
||||
return color;
|
||||
// Input colors are assumed to be in sRGB space,
|
||||
// but blending and rendering needs to happen in linear space.
|
||||
// The output will be converted to sRGB by either the target
|
||||
// texture format or the swapchain color space.
|
||||
let linear = srgb_to_linear(color);
|
||||
return vec4<f32>(linear, a);
|
||||
}
|
||||
|
||||
fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
|
||||
@@ -181,7 +193,8 @@ fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
|
||||
// target alpha compositing mode.
|
||||
fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
|
||||
let alpha = color.a * alpha_factor;
|
||||
return select(vec4<f32>(color.rgb, alpha), vec4<f32>(color.rgb, 1.0) * alpha, globals.premultiplied_alpha != 0u);
|
||||
let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
|
||||
return vec4<f32>(color.rgb * multiplier, alpha);
|
||||
}
|
||||
|
||||
// --- quads --- //
|
||||
|
||||
@@ -136,17 +136,21 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.with_common(|common| common.signal.stop());
|
||||
}
|
||||
|
||||
fn restart(&self) {
|
||||
fn restart(&self, binary_path: Option<PathBuf>) {
|
||||
use std::os::unix::process::CommandExt as _;
|
||||
|
||||
// get the process id of the current process
|
||||
let app_pid = std::process::id().to_string();
|
||||
// get the path to the executable
|
||||
let app_path = match self.app_path() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
log::error!("Failed to get app path: {:?}", err);
|
||||
return;
|
||||
let app_path = if let Some(path) = binary_path {
|
||||
path
|
||||
} else {
|
||||
match self.app_path() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
log::error!("Failed to get app path: {:?}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -396,7 +396,7 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn restart(&self) {
|
||||
fn restart(&self, _binary_path: Option<PathBuf>) {
|
||||
use std::os::unix::process::CommandExt as _;
|
||||
|
||||
let app_pid = std::process::id().to_string();
|
||||
|
||||
@@ -890,7 +890,7 @@ impl PlatformWindow for MacWindow {
|
||||
let alert_style = match level {
|
||||
PromptLevel::Info => 1,
|
||||
PromptLevel::Warning => 0,
|
||||
PromptLevel::Critical | PromptLevel::Destructive => 2,
|
||||
PromptLevel::Critical => 2,
|
||||
};
|
||||
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
||||
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
|
||||
@@ -905,16 +905,10 @@ impl PlatformWindow for MacWindow {
|
||||
{
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
if level == PromptLevel::Destructive && answer != &"Cancel" {
|
||||
let _: () = msg_send![button, setHasDestructiveAction: YES];
|
||||
}
|
||||
}
|
||||
if let Some((ix, answer)) = latest_non_cancel_label {
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
if level == PromptLevel::Destructive {
|
||||
let _: () = msg_send![button, setHasDestructiveAction: YES];
|
||||
}
|
||||
}
|
||||
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
|
||||
@@ -140,7 +140,7 @@ impl Platform for TestPlatform {
|
||||
|
||||
fn quit(&self) {}
|
||||
|
||||
fn restart(&self) {
|
||||
fn restart(&self, _: Option<PathBuf>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,14 @@ struct DirectWriteState {
|
||||
custom_font_collection: IDWriteFontCollection1,
|
||||
fonts: Vec<FontInfo>,
|
||||
font_selections: HashMap<Font, FontId>,
|
||||
font_id_by_postscript_name: HashMap<String, FontId>,
|
||||
font_id_by_identifier: HashMap<FontIdentifier, FontId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
struct FontIdentifier {
|
||||
postscript_name: String,
|
||||
weight: i32,
|
||||
style: i32,
|
||||
}
|
||||
|
||||
impl DirectWriteComponent {
|
||||
@@ -118,7 +125,7 @@ impl DirectWriteTextSystem {
|
||||
custom_font_collection,
|
||||
fonts: Vec::new(),
|
||||
font_selections: HashMap::default(),
|
||||
font_id_by_postscript_name: HashMap::default(),
|
||||
font_id_by_identifier: HashMap::default(),
|
||||
})))
|
||||
}
|
||||
}
|
||||
@@ -269,8 +276,7 @@ impl DirectWriteState {
|
||||
let Some(font_face) = font_face_ref.CreateFontFace().log_err() else {
|
||||
continue;
|
||||
};
|
||||
let Some(postscript_name) = get_postscript_name(&font_face, &self.components.locale)
|
||||
else {
|
||||
let Some(identifier) = get_font_identifier(&font_face, &self.components.locale) else {
|
||||
continue;
|
||||
};
|
||||
let is_emoji = font_face.IsColorFont().as_bool();
|
||||
@@ -287,8 +293,7 @@ impl DirectWriteState {
|
||||
};
|
||||
let font_id = FontId(self.fonts.len());
|
||||
self.fonts.push(font_info);
|
||||
self.font_id_by_postscript_name
|
||||
.insert(postscript_name, font_id);
|
||||
self.font_id_by_identifier.insert(identifier, font_id);
|
||||
return Some(font_id);
|
||||
}
|
||||
None
|
||||
@@ -945,8 +950,8 @@ impl IDWriteTextRenderer_Impl for TextRenderer {
|
||||
// This `cast()` action here should never fail since we are running on Win10+, and
|
||||
// `IDWriteFontFace3` requires Win10
|
||||
let font_face = &font_face.cast::<IDWriteFontFace3>().unwrap();
|
||||
let Some((postscript_name, font_struct, is_emoji)) =
|
||||
get_postscript_name_and_font(font_face, &self.locale)
|
||||
let Some((font_identifier, font_struct, is_emoji)) =
|
||||
get_font_identifier_and_font_struct(font_face, &self.locale)
|
||||
else {
|
||||
log::error!("none postscript name found");
|
||||
return Ok(());
|
||||
@@ -954,8 +959,8 @@ impl IDWriteTextRenderer_Impl for TextRenderer {
|
||||
|
||||
let font_id = if let Some(id) = context
|
||||
.text_system
|
||||
.font_id_by_postscript_name
|
||||
.get(&postscript_name)
|
||||
.font_id_by_identifier
|
||||
.get(&font_identifier)
|
||||
{
|
||||
*id
|
||||
} else {
|
||||
@@ -1121,39 +1126,60 @@ fn get_font_names_from_collection(
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_postscript_name_and_font(
|
||||
fn get_font_identifier_and_font_struct(
|
||||
font_face: &IDWriteFontFace3,
|
||||
locale: &str,
|
||||
) -> Option<(String, Font, bool)> {
|
||||
) -> Option<(FontIdentifier, Font, bool)> {
|
||||
let Some(postscript_name) = get_postscript_name(font_face, locale) else {
|
||||
return None;
|
||||
};
|
||||
let Some(localized_family_name) = font_face.GetFamilyNames().log_err() else {
|
||||
let Some(localized_family_name) = (unsafe { font_face.GetFamilyNames().log_err() }) else {
|
||||
return None;
|
||||
};
|
||||
let Some(family_name) = get_name(localized_family_name, locale) else {
|
||||
return None;
|
||||
};
|
||||
let weight = unsafe { font_face.GetWeight() };
|
||||
let style = unsafe { font_face.GetStyle() };
|
||||
let identifier = FontIdentifier {
|
||||
postscript_name,
|
||||
weight: weight.0,
|
||||
style: style.0,
|
||||
};
|
||||
let font_struct = Font {
|
||||
family: family_name.into(),
|
||||
features: FontFeatures::default(),
|
||||
weight: font_face.GetWeight().into(),
|
||||
style: font_face.GetStyle().into(),
|
||||
weight: weight.into(),
|
||||
style: style.into(),
|
||||
};
|
||||
let is_emoji = font_face.IsColorFont().as_bool();
|
||||
Some((postscript_name, font_struct, is_emoji))
|
||||
let is_emoji = unsafe { font_face.IsColorFont().as_bool() };
|
||||
Some((identifier, font_struct, is_emoji))
|
||||
}
|
||||
|
||||
unsafe fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Option<String> {
|
||||
let mut info = std::mem::zeroed();
|
||||
#[inline]
|
||||
fn get_font_identifier(font_face: &IDWriteFontFace3, locale: &str) -> Option<FontIdentifier> {
|
||||
let weight = unsafe { font_face.GetWeight().0 };
|
||||
let style = unsafe { font_face.GetStyle().0 };
|
||||
get_postscript_name(font_face, locale).map(|postscript_name| FontIdentifier {
|
||||
postscript_name,
|
||||
weight,
|
||||
style,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Option<String> {
|
||||
let mut info = None;
|
||||
let mut exists = BOOL(0);
|
||||
font_face
|
||||
.GetInformationalStrings(
|
||||
DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME,
|
||||
&mut info,
|
||||
&mut exists,
|
||||
)
|
||||
.log_err();
|
||||
unsafe {
|
||||
font_face
|
||||
.GetInformationalStrings(
|
||||
DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME,
|
||||
&mut info,
|
||||
&mut exists,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
if !exists.as_bool() || info.is_none() {
|
||||
return None;
|
||||
}
|
||||
@@ -1162,7 +1188,7 @@ unsafe fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Opt
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_font_feature_tag
|
||||
unsafe fn apply_font_features(
|
||||
fn apply_font_features(
|
||||
direct_write_features: &IDWriteTypography,
|
||||
features: &FontFeatures,
|
||||
) -> Result<()> {
|
||||
@@ -1191,11 +1217,15 @@ unsafe fn apply_font_features(
|
||||
continue;
|
||||
}
|
||||
|
||||
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
|
||||
unsafe {
|
||||
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
direct_write_features.AddFontFeature(feature_liga)?;
|
||||
direct_write_features.AddFontFeature(feature_clig)?;
|
||||
direct_write_features.AddFontFeature(feature_calt)?;
|
||||
}
|
||||
direct_write_features.AddFontFeature(feature_liga)?;
|
||||
direct_write_features.AddFontFeature(feature_clig)?;
|
||||
direct_write_features.AddFontFeature(feature_calt)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1231,32 +1261,39 @@ fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG {
|
||||
DWRITE_FONT_FEATURE_TAG(make_open_type_tag(tag_name))
|
||||
}
|
||||
|
||||
unsafe fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Option<String> {
|
||||
#[inline]
|
||||
fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Option<String> {
|
||||
let mut locale_name_index = 0u32;
|
||||
let mut exists = BOOL(0);
|
||||
string
|
||||
.FindLocaleName(
|
||||
&HSTRING::from(locale),
|
||||
&mut locale_name_index,
|
||||
&mut exists as _,
|
||||
)
|
||||
.log_err();
|
||||
if !exists.as_bool() {
|
||||
unsafe {
|
||||
string
|
||||
.FindLocaleName(
|
||||
DEFAULT_LOCALE_NAME,
|
||||
&mut locale_name_index as _,
|
||||
&HSTRING::from(locale),
|
||||
&mut locale_name_index,
|
||||
&mut exists as _,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
if !exists.as_bool() {
|
||||
unsafe {
|
||||
string
|
||||
.FindLocaleName(
|
||||
DEFAULT_LOCALE_NAME,
|
||||
&mut locale_name_index as _,
|
||||
&mut exists as _,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
if !exists.as_bool() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let name_length = string.GetStringLength(locale_name_index).unwrap() as usize;
|
||||
let name_length = unsafe { string.GetStringLength(locale_name_index).unwrap() } as usize;
|
||||
let mut name_vec = vec![0u16; name_length + 1];
|
||||
string.GetString(locale_name_index, &mut name_vec).unwrap();
|
||||
unsafe {
|
||||
string.GetString(locale_name_index, &mut name_vec).unwrap();
|
||||
}
|
||||
|
||||
Some(String::from_utf16_lossy(&name_vec[..name_length]))
|
||||
}
|
||||
@@ -1287,7 +1324,8 @@ fn get_system_ui_font_name() -> SharedString {
|
||||
// Segoe UI is the Windows font intended for user interface text strings.
|
||||
"Segoe UI".into()
|
||||
} else {
|
||||
String::from_utf16_lossy(&info.lfFaceName).into()
|
||||
let font_name = String::from_utf16_lossy(&info.lfFaceName);
|
||||
font_name.trim_matches(char::from(0)).to_owned().into()
|
||||
};
|
||||
log::info!("Use {} as UI font.", font_family);
|
||||
font_family
|
||||
|
||||
@@ -1,71 +1,97 @@
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicIsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::{current, ThreadId},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_task::Runnable;
|
||||
use flume::Sender;
|
||||
use parking::Parker;
|
||||
use parking_lot::Mutex;
|
||||
use windows::Win32::{Foundation::*, System::Threading::*};
|
||||
use util::ResultExt;
|
||||
use windows::{
|
||||
Foundation::TimeSpan,
|
||||
System::{
|
||||
DispatcherQueue, DispatcherQueueController, DispatcherQueueHandler,
|
||||
Threading::{
|
||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
|
||||
WorkItemPriority,
|
||||
},
|
||||
},
|
||||
Win32::System::WinRT::{
|
||||
CreateDispatcherQueueController, DispatcherQueueOptions, DQTAT_COM_NONE,
|
||||
DQTYPE_THREAD_CURRENT,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
|
||||
pub(crate) struct WindowsDispatcher {
|
||||
threadpool: PTP_POOL,
|
||||
main_sender: Sender<Runnable>,
|
||||
controller: DispatcherQueueController,
|
||||
main_queue: DispatcherQueue,
|
||||
parker: Mutex<Parker>,
|
||||
main_thread_id: ThreadId,
|
||||
dispatch_event: HANDLE,
|
||||
}
|
||||
|
||||
impl WindowsDispatcher {
|
||||
pub(crate) fn new(main_sender: Sender<Runnable>, dispatch_event: HANDLE) -> Self {
|
||||
let parker = Mutex::new(Parker::new());
|
||||
let threadpool = unsafe {
|
||||
let ret = CreateThreadpool(None);
|
||||
if ret.0 == 0 {
|
||||
panic!(
|
||||
"unable to initialize a thread pool: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
// set minimum 1 thread in threadpool
|
||||
let _ = SetThreadpoolThreadMinimum(ret, 1)
|
||||
.inspect_err(|_| log::error!("unable to configure thread pool"));
|
||||
unsafe impl Send for WindowsDispatcher {}
|
||||
unsafe impl Sync for WindowsDispatcher {}
|
||||
|
||||
ret
|
||||
impl WindowsDispatcher {
|
||||
pub(crate) fn new() -> Self {
|
||||
let controller = unsafe {
|
||||
let options = DispatcherQueueOptions {
|
||||
dwSize: std::mem::size_of::<DispatcherQueueOptions>() as u32,
|
||||
threadType: DQTYPE_THREAD_CURRENT,
|
||||
apartmentType: DQTAT_COM_NONE,
|
||||
};
|
||||
CreateDispatcherQueueController(options).unwrap()
|
||||
};
|
||||
let main_queue = controller.DispatcherQueue().unwrap();
|
||||
let parker = Mutex::new(Parker::new());
|
||||
let main_thread_id = current().id();
|
||||
|
||||
WindowsDispatcher {
|
||||
threadpool,
|
||||
main_sender,
|
||||
controller,
|
||||
main_queue,
|
||||
parker,
|
||||
main_thread_id,
|
||||
dispatch_event,
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_on_threadpool(&self, runnable: Runnable) {
|
||||
unsafe {
|
||||
let ptr = Box::into_raw(Box::new(runnable));
|
||||
let environment = get_threadpool_environment(self.threadpool);
|
||||
let Ok(work) =
|
||||
CreateThreadpoolWork(Some(threadpool_runner), Some(ptr as _), Some(&environment))
|
||||
.inspect_err(|_| {
|
||||
log::error!(
|
||||
"unable to dispatch work on thread pool: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
SubmitThreadpoolWork(work);
|
||||
}
|
||||
let handler = {
|
||||
let mut task_wrapper = Some(runnable);
|
||||
WorkItemHandler::new(move |_| {
|
||||
task_wrapper.take().unwrap().run();
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
ThreadPool::RunWithPriorityAndOptionsAsync(
|
||||
&handler,
|
||||
WorkItemPriority::High,
|
||||
WorkItemOptions::TimeSliced,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
|
||||
let handler = {
|
||||
let mut task_wrapper = Some(runnable);
|
||||
TimerElapsedHandler::new(move |_| {
|
||||
task_wrapper.take().unwrap().run();
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
let delay = TimeSpan {
|
||||
// A time period expressed in 100-nanosecond units.
|
||||
// 10,000,000 ticks per second
|
||||
Duration: (duration.as_nanos() / 100) as i64,
|
||||
};
|
||||
ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WindowsDispatcher {
|
||||
fn drop(&mut self) {
|
||||
self.controller.ShutdownQueueAsync().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,38 +108,18 @@ impl PlatformDispatcher for WindowsDispatcher {
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||
self.main_sender
|
||||
.send(runnable)
|
||||
.inspect_err(|e| log::error!("Dispatch failed: {e}"))
|
||||
.ok();
|
||||
unsafe { SetEvent(self.dispatch_event) }.ok();
|
||||
let handler = {
|
||||
let mut task_wrapper = Some(runnable);
|
||||
DispatcherQueueHandler::new(move || {
|
||||
task_wrapper.take().unwrap().run();
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
self.main_queue.TryEnqueue(&handler).log_err();
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) {
|
||||
if duration.as_millis() == 0 {
|
||||
self.dispatch_on_threadpool(runnable);
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let mut handle = std::mem::zeroed();
|
||||
let task = Arc::new(DelayedTask::new(runnable));
|
||||
let _ = CreateTimerQueueTimer(
|
||||
&mut handle,
|
||||
None,
|
||||
Some(timer_queue_runner),
|
||||
Some(Arc::into_raw(task.clone()) as _),
|
||||
duration.as_millis() as u32,
|
||||
0,
|
||||
WT_EXECUTEONLYONCE,
|
||||
)
|
||||
.inspect_err(|_| {
|
||||
log::error!(
|
||||
"unable to dispatch delayed task: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)
|
||||
});
|
||||
task.raw_timer_handle.store(handle.0, Ordering::SeqCst);
|
||||
}
|
||||
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
|
||||
self.dispatch_on_threadpool_after(runnable, duration);
|
||||
}
|
||||
|
||||
fn tick(&self, _background_only: bool) -> bool {
|
||||
@@ -128,48 +134,3 @@ impl PlatformDispatcher for WindowsDispatcher {
|
||||
self.parker.lock().unparker()
|
||||
}
|
||||
}
|
||||
|
||||
extern "system" fn threadpool_runner(
|
||||
_: PTP_CALLBACK_INSTANCE,
|
||||
ptr: *mut std::ffi::c_void,
|
||||
_: PTP_WORK,
|
||||
) {
|
||||
unsafe {
|
||||
let runnable = Box::from_raw(ptr as *mut Runnable);
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "system" fn timer_queue_runner(ptr: *mut std::ffi::c_void, _: BOOLEAN) {
|
||||
let task = Arc::from_raw(ptr as *mut DelayedTask);
|
||||
task.runnable.lock().take().unwrap().run();
|
||||
unsafe {
|
||||
let timer = task.raw_timer_handle.load(Ordering::SeqCst);
|
||||
let _ = DeleteTimerQueueTimer(None, HANDLE(timer), None);
|
||||
}
|
||||
}
|
||||
|
||||
struct DelayedTask {
|
||||
runnable: Mutex<Option<Runnable>>,
|
||||
raw_timer_handle: AtomicIsize,
|
||||
}
|
||||
|
||||
impl DelayedTask {
|
||||
pub fn new(runnable: Runnable) -> Self {
|
||||
DelayedTask {
|
||||
runnable: Mutex::new(Some(runnable)),
|
||||
raw_timer_handle: AtomicIsize::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_threadpool_environment(pool: PTP_POOL) -> TP_CALLBACK_ENVIRON_V3 {
|
||||
TP_CALLBACK_ENVIRON_V3 {
|
||||
Version: 3, // Win7+, otherwise this value should be 1
|
||||
Pool: pool,
|
||||
CallbackPriority: TP_CALLBACK_PRIORITY_NORMAL,
|
||||
Size: std::mem::size_of::<TP_CALLBACK_ENVIRON_V3>() as _,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,9 +171,6 @@ fn handle_timer_msg(
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID {
|
||||
for runnable in state_ptr.main_receiver.drain() {
|
||||
runnable.run();
|
||||
}
|
||||
handle_paint_msg(handle, state_ptr)
|
||||
} else {
|
||||
None
|
||||
@@ -547,7 +544,7 @@ fn handle_mouse_horizontal_wheel_msg(
|
||||
let wheel_scroll_chars = lock.mouse_wheel_settings.wheel_scroll_chars;
|
||||
drop(lock);
|
||||
let wheel_distance =
|
||||
(wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
|
||||
(-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
@@ -585,7 +582,10 @@ fn handle_ime_position(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
|
||||
let caret_range = input_handler.selected_text_range().unwrap_or_default();
|
||||
let Some(caret_range) = input_handler.selected_text_range() else {
|
||||
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
|
||||
return Some(0);
|
||||
};
|
||||
let caret_position = input_handler.bounds_for_range(caret_range).unwrap();
|
||||
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
|
||||
let config = CANDIDATEFORM {
|
||||
|
||||
@@ -13,7 +13,6 @@ use std::{
|
||||
|
||||
use ::util::ResultExt;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_task::Runnable;
|
||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use itertools::Itertools;
|
||||
@@ -42,11 +41,9 @@ pub(crate) struct WindowsPlatform {
|
||||
raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
|
||||
// The below members will never change throughout the entire lifecycle of the app.
|
||||
icon: HICON,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
dispatch_event: OwnedHandle,
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsPlatformState {
|
||||
@@ -85,10 +82,7 @@ impl WindowsPlatform {
|
||||
unsafe {
|
||||
OleInitialize(None).expect("unable to initialize Windows OLE");
|
||||
}
|
||||
let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
|
||||
let dispatch_event =
|
||||
OwnedHandle::new(unsafe { CreateEventW(None, false, false, None) }.unwrap());
|
||||
let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, dispatch_event.to_raw()));
|
||||
let dispatcher = Arc::new(WindowsDispatcher::new());
|
||||
let background_executor = BackgroundExecutor::new(dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
||||
let text_system = if let Some(direct_write) = DirectWriteTextSystem::new().log_err() {
|
||||
@@ -106,18 +100,9 @@ impl WindowsPlatform {
|
||||
state,
|
||||
raw_window_handles,
|
||||
icon,
|
||||
main_receiver,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
text_system,
|
||||
dispatch_event,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn run_foreground_tasks(&self) {
|
||||
for runnable in self.main_receiver.drain() {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +186,6 @@ impl Platform for WindowsPlatform {
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
||||
on_finish_launching();
|
||||
let dispatch_event = self.dispatch_event.to_raw();
|
||||
let vsync_event = create_event().unwrap();
|
||||
let timer_stop_event = create_event().unwrap();
|
||||
let raw_timer_stop_event = timer_stop_event.to_raw();
|
||||
@@ -209,7 +193,7 @@ impl Platform for WindowsPlatform {
|
||||
'a: loop {
|
||||
let wait_result = unsafe {
|
||||
MsgWaitForMultipleObjects(
|
||||
Some(&[vsync_event.to_raw(), dispatch_event]),
|
||||
Some(&[vsync_event.to_raw()]),
|
||||
false,
|
||||
INFINITE,
|
||||
QS_ALLINPUT,
|
||||
@@ -221,12 +205,8 @@ impl Platform for WindowsPlatform {
|
||||
WAIT_EVENT(0) => {
|
||||
self.redraw_all();
|
||||
}
|
||||
// foreground tasks are dispatched
|
||||
WAIT_EVENT(1) => {
|
||||
self.run_foreground_tasks();
|
||||
}
|
||||
// Windows thread messages are posted
|
||||
WAIT_EVENT(2) => {
|
||||
WAIT_EVENT(1) => {
|
||||
let mut msg = MSG::default();
|
||||
unsafe {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
@@ -245,9 +225,6 @@ impl Platform for WindowsPlatform {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// foreground tasks may have been queued in the message handlers
|
||||
self.run_foreground_tasks();
|
||||
}
|
||||
_ => {
|
||||
log::error!("Something went wrong while waiting {:?}", wait_result);
|
||||
@@ -268,7 +245,7 @@ impl Platform for WindowsPlatform {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn restart(&self) {
|
||||
fn restart(&self, _: Option<PathBuf>) {
|
||||
let pid = std::process::id();
|
||||
let Some(app_path) = self.app_path().log_err() else {
|
||||
return;
|
||||
@@ -344,7 +321,6 @@ impl Platform for WindowsPlatform {
|
||||
options,
|
||||
self.icon,
|
||||
self.foreground_executor.clone(),
|
||||
self.main_receiver.clone(),
|
||||
lock.settings.mouse_wheel_settings,
|
||||
lock.current_cursor,
|
||||
);
|
||||
@@ -684,7 +660,7 @@ impl Platform for WindowsPlatform {
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let mut ctx = ClipboardContext::new().unwrap();
|
||||
let content = ctx.get_contents().unwrap();
|
||||
let content = ctx.get_contents().ok()?;
|
||||
Some(ClipboardItem {
|
||||
text: content,
|
||||
metadata: None,
|
||||
@@ -813,8 +789,8 @@ unsafe fn show_savefile_dialog(directory: PathBuf) -> Result<IFileSaveDialog> {
|
||||
|
||||
fn begin_vsync_timer(vsync_event: HANDLE, timer_stop_event: OwnedHandle) {
|
||||
let vsync_fn = select_vsync_fn();
|
||||
std::thread::spawn(move || {
|
||||
while vsync_fn(timer_stop_event.to_raw()) {
|
||||
std::thread::spawn(move || loop {
|
||||
if vsync_fn(timer_stop_event.to_raw()) {
|
||||
if unsafe { SetEvent(vsync_event) }.log_err().is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -118,12 +118,14 @@ pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
|
||||
CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => (&IBEAM, IDC_IBEAM),
|
||||
CursorStyle::Crosshair => (&CROSS, IDC_CROSS),
|
||||
CursorStyle::PointingHand | CursorStyle::DragLink => (&HAND, IDC_HAND),
|
||||
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
|
||||
(&SIZEWE, IDC_SIZEWE)
|
||||
}
|
||||
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
|
||||
(&SIZENS, IDC_SIZENS)
|
||||
}
|
||||
CursorStyle::ResizeLeft
|
||||
| CursorStyle::ResizeRight
|
||||
| CursorStyle::ResizeLeftRight
|
||||
| CursorStyle::ResizeColumn => (&SIZEWE, IDC_SIZEWE),
|
||||
CursorStyle::ResizeUp
|
||||
| CursorStyle::ResizeDown
|
||||
| CursorStyle::ResizeUpDown
|
||||
| CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS),
|
||||
CursorStyle::OperationNotAllowed => (&NO, IDC_NO),
|
||||
_ => (&ARROW, IDC_ARROW),
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ use std::{
|
||||
|
||||
use ::util::ResultExt;
|
||||
use anyhow::Context;
|
||||
use async_task::Runnable;
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use itertools::Itertools;
|
||||
use raw_window_handle as rwh;
|
||||
@@ -58,7 +57,6 @@ pub(crate) struct WindowsWindowStatePtr {
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) hide_title_bar: bool,
|
||||
pub(crate) executor: ForegroundExecutor,
|
||||
pub(crate) main_receiver: flume::Receiver<Runnable>,
|
||||
}
|
||||
|
||||
impl WindowsWindowState {
|
||||
@@ -208,7 +206,6 @@ impl WindowsWindowStatePtr {
|
||||
handle: context.handle,
|
||||
hide_title_bar: context.hide_title_bar,
|
||||
executor: context.executor.clone(),
|
||||
main_receiver: context.main_receiver.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -232,7 +229,6 @@ struct WindowCreateContext {
|
||||
display: WindowsDisplay,
|
||||
transparent: bool,
|
||||
executor: ForegroundExecutor,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
mouse_wheel_settings: MouseWheelSettings,
|
||||
current_cursor: HCURSOR,
|
||||
}
|
||||
@@ -243,7 +239,6 @@ impl WindowsWindow {
|
||||
params: WindowParams,
|
||||
icon: HICON,
|
||||
executor: ForegroundExecutor,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
mouse_wheel_settings: MouseWheelSettings,
|
||||
current_cursor: HCURSOR,
|
||||
) -> Self {
|
||||
@@ -272,7 +267,6 @@ impl WindowsWindow {
|
||||
display: WindowsDisplay::primary_monitor().unwrap(),
|
||||
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
|
||||
executor,
|
||||
main_receiver,
|
||||
mouse_wheel_settings,
|
||||
current_cursor,
|
||||
};
|
||||
@@ -443,7 +437,7 @@ impl PlatformWindow for WindowsWindow {
|
||||
title = windows::core::w!("Warning");
|
||||
main_icon = TD_WARNING_ICON;
|
||||
}
|
||||
crate::PromptLevel::Critical | crate::PromptLevel::Destructive => {
|
||||
crate::PromptLevel::Critical => {
|
||||
title = windows::core::w!("Critical");
|
||||
main_icon = TD_ERROR_ICON;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl TaffyLayoutEngine {
|
||||
|
||||
pub fn request_layout(
|
||||
&mut self,
|
||||
style: &Style,
|
||||
style: Style,
|
||||
rem_size: Pixels,
|
||||
children: &[LayoutId],
|
||||
) -> LayoutId {
|
||||
@@ -66,12 +66,11 @@ impl TaffyLayoutEngine {
|
||||
.new_with_children(taffy_style, unsafe { std::mem::transmute(children) })
|
||||
.expect(EXPECT_MESSAGE)
|
||||
.into();
|
||||
for child_id in children {
|
||||
self.children_to_parents.insert(*child_id, parent_id);
|
||||
}
|
||||
self.children_to_parents
|
||||
.extend(children.into_iter().map(|child_id| (*child_id, parent_id)));
|
||||
parent_id
|
||||
};
|
||||
self.styles.insert(layout_id, style.clone());
|
||||
self.styles.insert(layout_id, style);
|
||||
layout_id
|
||||
}
|
||||
|
||||
@@ -82,7 +81,6 @@ impl TaffyLayoutEngine {
|
||||
measure: impl FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut WindowContext) -> Size<Pixels>
|
||||
+ 'static,
|
||||
) -> LayoutId {
|
||||
let style = style.clone();
|
||||
let taffy_style = style.to_taffy(rem_size);
|
||||
|
||||
let layout_id = self
|
||||
@@ -91,7 +89,7 @@ impl TaffyLayoutEngine {
|
||||
.expect(EXPECT_MESSAGE)
|
||||
.into();
|
||||
self.nodes_to_measure.insert(layout_id, Box::new(measure));
|
||||
self.styles.insert(layout_id, style.clone());
|
||||
self.styles.insert(layout_id, style);
|
||||
layout_id
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
|
||||
use crate::{point, px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
|
||||
use collections::FxHashMap;
|
||||
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
||||
use smallvec::SmallVec;
|
||||
@@ -254,39 +254,83 @@ impl WrappedLineLayout {
|
||||
/// The index corresponding to a given position in this layout for the given line height.
|
||||
pub fn index_for_position(
|
||||
&self,
|
||||
position: Point<Pixels>,
|
||||
mut position: Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
) -> Option<usize> {
|
||||
) -> Result<usize, usize> {
|
||||
let wrapped_line_ix = (position.y / line_height) as usize;
|
||||
|
||||
let wrapped_line_start_x = if wrapped_line_ix > 0 {
|
||||
let wrapped_line_start_index;
|
||||
let wrapped_line_start_x;
|
||||
if wrapped_line_ix > 0 {
|
||||
let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
|
||||
return None;
|
||||
return Err(0);
|
||||
};
|
||||
let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
|
||||
run.glyphs[line_start_boundary.glyph_ix].position.x
|
||||
let glyph = &run.glyphs[line_start_boundary.glyph_ix];
|
||||
wrapped_line_start_index = glyph.index;
|
||||
wrapped_line_start_x = glyph.position.x;
|
||||
} else {
|
||||
Pixels::ZERO
|
||||
wrapped_line_start_index = 0;
|
||||
wrapped_line_start_x = Pixels::ZERO;
|
||||
};
|
||||
|
||||
let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() {
|
||||
let wrapped_line_end_index;
|
||||
let wrapped_line_end_x;
|
||||
if wrapped_line_ix < self.wrap_boundaries.len() {
|
||||
let next_wrap_boundary_ix = wrapped_line_ix;
|
||||
let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
|
||||
let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
|
||||
run.glyphs[next_wrap_boundary.glyph_ix].position.x
|
||||
let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
|
||||
wrapped_line_end_index = glyph.index;
|
||||
wrapped_line_end_x = glyph.position.x;
|
||||
} else {
|
||||
self.unwrapped_layout.width
|
||||
wrapped_line_end_index = self.unwrapped_layout.len;
|
||||
wrapped_line_end_x = self.unwrapped_layout.width;
|
||||
};
|
||||
|
||||
let mut position_in_unwrapped_line = position;
|
||||
position_in_unwrapped_line.x += wrapped_line_start_x;
|
||||
if position_in_unwrapped_line.x > wrapped_line_end_x {
|
||||
None
|
||||
if position_in_unwrapped_line.x < wrapped_line_start_x {
|
||||
Err(wrapped_line_start_index)
|
||||
} else if position_in_unwrapped_line.x >= wrapped_line_end_x {
|
||||
Err(wrapped_line_end_index)
|
||||
} else {
|
||||
self.unwrapped_layout
|
||||
Ok(self
|
||||
.unwrapped_layout
|
||||
.index_for_x(position_in_unwrapped_line.x)
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
|
||||
let mut line_start_ix = 0;
|
||||
let mut line_end_indices = self
|
||||
.wrap_boundaries
|
||||
.iter()
|
||||
.map(|wrap_boundary| {
|
||||
let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
|
||||
let glyph = &run.glyphs[wrap_boundary.glyph_ix];
|
||||
glyph.index
|
||||
})
|
||||
.chain([self.len()])
|
||||
.enumerate();
|
||||
for (ix, line_end_ix) in line_end_indices {
|
||||
let line_y = ix as f32 * line_height;
|
||||
if index < line_start_ix {
|
||||
break;
|
||||
} else if index > line_end_ix {
|
||||
line_start_ix = line_end_ix;
|
||||
continue;
|
||||
} else {
|
||||
let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
|
||||
let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
|
||||
return Some(point(x, line_y));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LineLayoutCache {
|
||||
|
||||
@@ -296,7 +296,7 @@ impl Element for AnyView {
|
||||
if let Some(style) = self.cached_style.as_ref() {
|
||||
let mut root_style = Style::default();
|
||||
root_style.refine(style);
|
||||
let layout_id = cx.request_layout(&root_style, None);
|
||||
let layout_id = cx.request_layout(root_style, None);
|
||||
(layout_id, None)
|
||||
} else {
|
||||
let mut element = (self.render)(self, cx);
|
||||
|
||||
@@ -2503,7 +2503,7 @@ impl<'a> WindowContext<'a> {
|
||||
/// This method should only be called as part of the request_layout or prepaint phase of element drawing.
|
||||
pub fn request_layout(
|
||||
&mut self,
|
||||
style: &Style,
|
||||
style: Style,
|
||||
children: impl IntoIterator<Item = LayoutId>,
|
||||
) -> LayoutId {
|
||||
debug_assert_eq!(
|
||||
|
||||
@@ -15,7 +15,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
ctrlc.workspace = true
|
||||
signal-hook.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
rpc.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::DevServerProjectId;
|
||||
use client::{user::UserStore, Client, ClientSettings};
|
||||
use fs::Fs;
|
||||
@@ -36,7 +36,7 @@ struct GlobalDevServer(Model<DevServer>);
|
||||
|
||||
impl Global for GlobalDevServer {}
|
||||
|
||||
pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
|
||||
pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let dev_server = cx.new_model(|cx| DevServer::new(client.clone(), app_state, cx));
|
||||
cx.set_global(GlobalDevServer(dev_server.clone()));
|
||||
|
||||
@@ -49,42 +49,36 @@ pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
|
||||
});
|
||||
});
|
||||
|
||||
// Set up a handler when the dev server is shut down by the user pressing Ctrl-C
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
set_ctrlc_handler(move || tx.send(()).log_err().unwrap()).log_err();
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
rx.await.log_err();
|
||||
log::info!("Received interrupt signal");
|
||||
cx.update(|cx| cx.quit()).log_err();
|
||||
})
|
||||
.detach();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
use signal_hook::consts::{SIGINT, SIGTERM};
|
||||
use signal_hook::iterator::Signals;
|
||||
// Set up a handler when the dev server is shut down
|
||||
// with ctrl-c or kill
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
let mut signals = Signals::new(&[SIGTERM, SIGINT]).unwrap();
|
||||
std::thread::spawn({
|
||||
move || {
|
||||
if let Some(sig) = signals.forever().next() {
|
||||
tx.send(sig).log_err();
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.spawn(|cx| async move {
|
||||
if let Ok(sig) = rx.await {
|
||||
log::info!("received signal {sig:?}");
|
||||
cx.update(|cx| cx.quit()).log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
let server_url = ClientSettings::get_global(&cx).server_url.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
match client.authenticate_and_connect(false, &cx).await {
|
||||
Ok(_) => {
|
||||
log::info!("Connected to {}", server_url);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error connecting to '{}': {}", server_url, e);
|
||||
cx.update(|cx| cx.quit()).log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_ctrlc_handler<F>(f: F) -> Result<(), ctrlc::Error>
|
||||
where
|
||||
F: FnOnce() + 'static + Send,
|
||||
{
|
||||
let f = std::sync::Mutex::new(Some(f));
|
||||
ctrlc::set_handler(move || {
|
||||
if let Ok(mut guard) = f.lock() {
|
||||
let f = guard.take().expect("f can only be taken once");
|
||||
f();
|
||||
}
|
||||
client
|
||||
.authenticate_and_connect(false, &cx)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Error connecting to '{}': {}", server_url, e))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -186,7 +180,7 @@ impl DevServer {
|
||||
|
||||
let path_exists = fs.is_dir(path).await;
|
||||
if !path_exists {
|
||||
return Err(anyhow::anyhow!(ErrorCode::DevServerProjectPathDoesNotExist))?;
|
||||
return Err(anyhow!(ErrorCode::DevServerProjectPathDoesNotExist))?;
|
||||
}
|
||||
|
||||
Ok(proto::Ack {})
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
||||
SyntaxSnapshot, ToTreeSitterPoint,
|
||||
},
|
||||
task_context::RunnableRange,
|
||||
LanguageScope, Outline, RunnableTag,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
@@ -2993,7 +2994,7 @@ impl BufferSnapshot {
|
||||
pub fn runnable_ranges(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
|
||||
) -> impl Iterator<Item = RunnableRange> + '_ {
|
||||
let offset_range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| {
|
||||
@@ -3007,31 +3008,49 @@ impl BufferSnapshot {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
iter::from_fn(move || {
|
||||
let test_range = syntax_matches
|
||||
.peek()
|
||||
.and_then(|mat| {
|
||||
test_configs[mat.grammar_index].and_then(|test_configs| {
|
||||
let tags = SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
|
||||
test_configs.runnable_tags.get(&capture.index).cloned()
|
||||
let test_range = syntax_matches.peek().and_then(|mat| {
|
||||
test_configs[mat.grammar_index].and_then(|test_configs| {
|
||||
let mut tags: SmallVec<[(Range<usize>, RunnableTag); 1]> =
|
||||
SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
|
||||
test_configs
|
||||
.runnable_tags
|
||||
.get(&capture.index)
|
||||
.cloned()
|
||||
.map(|tag_name| (capture.node.byte_range(), tag_name))
|
||||
}));
|
||||
|
||||
if tags.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((
|
||||
mat.captures
|
||||
.iter()
|
||||
.find(|capture| capture.index == test_configs.run_capture_ix)?,
|
||||
Runnable {
|
||||
tags,
|
||||
language: mat.language,
|
||||
buffer: self.remote_id(),
|
||||
},
|
||||
))
|
||||
let maximum_range = tags
|
||||
.iter()
|
||||
.max_by_key(|(byte_range, _)| byte_range.len())
|
||||
.map(|(range, _)| range)?
|
||||
.clone();
|
||||
tags.sort_by_key(|(range, _)| range == &maximum_range);
|
||||
let split_point = tags.partition_point(|(range, _)| range != &maximum_range);
|
||||
let (extra_captures, tags) = tags.split_at(split_point);
|
||||
let extra_captures = extra_captures
|
||||
.into_iter()
|
||||
.map(|(range, name)| {
|
||||
(
|
||||
name.0.to_string(),
|
||||
self.text_for_range(range.clone()).collect::<String>(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Some(RunnableRange {
|
||||
run_range: mat
|
||||
.captures
|
||||
.iter()
|
||||
.find(|capture| capture.index == test_configs.run_capture_ix)
|
||||
.map(|mat| mat.node.byte_range())?,
|
||||
runnable: Runnable {
|
||||
tags: tags.into_iter().cloned().map(|(_, tag)| tag).collect(),
|
||||
language: mat.language,
|
||||
buffer: self.remote_id(),
|
||||
},
|
||||
extra_captures,
|
||||
buffer_id: self.remote_id(),
|
||||
})
|
||||
})
|
||||
.map(|(mat, test_tags)| (mat.node.byte_range(), test_tags));
|
||||
});
|
||||
syntax_matches.advance();
|
||||
test_range
|
||||
})
|
||||
|
||||
@@ -57,7 +57,9 @@ use std::{
|
||||
};
|
||||
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
|
||||
use task::RunnableTag;
|
||||
pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
|
||||
pub use task_context::{
|
||||
BasicContextProvider, ContextProvider, ContextProviderWithTasks, RunnableRange,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
|
||||
use util::http::HttpClient;
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
use std::path::Path;
|
||||
use std::{ops::Range, path::Path};
|
||||
|
||||
use crate::Location;
|
||||
use crate::{Location, Runnable};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::AppContext;
|
||||
use task::{TaskTemplates, TaskVariables, VariableName};
|
||||
use text::{Point, ToPoint};
|
||||
use text::{BufferId, Point, ToPoint};
|
||||
|
||||
pub struct RunnableRange {
|
||||
pub buffer_id: BufferId,
|
||||
pub run_range: Range<usize>,
|
||||
pub runnable: Runnable,
|
||||
pub extra_captures: HashMap<String, String>,
|
||||
}
|
||||
/// Language Contexts are used by Zed tasks to extract information about the source file where the tasks are supposed to be scheduled from.
|
||||
/// Multiple context providers may be used together: by default, Zed provides a base [`BasicContextProvider`] context that fills all non-custom [`VariableName`] variants.
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
(
|
||||
(attribute_item (attribute) @_attribute
|
||||
(#match? @_attribute ".*test"))
|
||||
(attribute_item (attribute) @attribute
|
||||
(#match? @attribute ".*test"))
|
||||
.
|
||||
(function_item
|
||||
name: (_) @run)
|
||||
|
||||
40
crates/markdown/Cargo.toml
Normal file
40
crates/markdown/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "markdown"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/markdown.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
"util/test-support"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
log.workspace = true
|
||||
pulldown-cmark.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assets.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
languages.workspace = true
|
||||
node_runtime.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
181
crates/markdown/examples/markdown.rs
Normal file
181
crates/markdown/examples/markdown.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use assets::Assets;
|
||||
use gpui::{prelude::*, App, Task, View, WindowOptions};
|
||||
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use theme::LoadThemes;
|
||||
use ui::prelude::*;
|
||||
use ui::{div, WindowContext};
|
||||
|
||||
const MARKDOWN_EXAMPLE: &'static str = r#"
|
||||
# Markdown Example Document
|
||||
|
||||
## Headings
|
||||
Headings are created by adding one or more `#` symbols before your heading text. The number of `#` you use will determine the size of the heading.
|
||||
|
||||
## Emphasis
|
||||
Emphasis can be added with italics or bold. *This text will be italic*. _This will also be italic_
|
||||
|
||||
## Lists
|
||||
|
||||
### Unordered Lists
|
||||
Unordered lists use asterisks `*`, plus `+`, or minus `-` as list markers.
|
||||
|
||||
* Item 1
|
||||
* Item 2
|
||||
* Item 2a
|
||||
* Item 2b
|
||||
|
||||
### Ordered Lists
|
||||
Ordered lists use numbers followed by a period.
|
||||
|
||||
1. Item 1
|
||||
2. Item 2
|
||||
3. Item 3
|
||||
1. Item 3a
|
||||
2. Item 3b
|
||||
|
||||
## Links
|
||||
Links are created using the format [http://zed.dev](https://zed.dev).
|
||||
|
||||
They can also be detected automatically, for example https://zed.dev/blog.
|
||||
|
||||
## Images
|
||||
Images are like links, but with an exclamation mark `!` in front.
|
||||
|
||||
```todo!
|
||||

|
||||
```
|
||||
|
||||
## Code
|
||||
Inline `code` can be wrapped with backticks `` ` ``.
|
||||
|
||||
```markdown
|
||||
Inline `code` has `back-ticks around` it.
|
||||
```
|
||||
|
||||
Code blocks can be created by indenting lines by four spaces or with triple backticks ```.
|
||||
|
||||
```javascript
|
||||
function test() {
|
||||
console.log("notice the blank line before this function?");
|
||||
}
|
||||
```
|
||||
|
||||
## Blockquotes
|
||||
Blockquotes are created with `>`.
|
||||
|
||||
> This is a blockquote.
|
||||
|
||||
## Horizontal Rules
|
||||
Horizontal rules are created using three or more asterisks `***`, dashes `---`, or underscores `___`.
|
||||
|
||||
## Line breaks
|
||||
This is a
|
||||
\
|
||||
line break!
|
||||
|
||||
---
|
||||
|
||||
Remember, markdown processors may have slight differences and extensions, so always refer to the specific documentation or guides relevant to your platform or editor for the best practices and additional features.
|
||||
"#;
|
||||
|
||||
pub fn main() {
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
language::init(cx);
|
||||
SettingsStore::update(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||
});
|
||||
|
||||
let node_runtime = FakeNodeRuntime::new();
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|cx| {
|
||||
MarkdownExample::new(
|
||||
MARKDOWN_EXAMPLE.to_string(),
|
||||
MarkdownStyle {
|
||||
code_block: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Mono".into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Mono".into()),
|
||||
// @nate: Could we add inline-code specific styles to the theme?
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Color::Muted.color(cx),
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: {
|
||||
let mut selection = cx.theme().players().local().selection;
|
||||
selection.fade_out(0.7);
|
||||
selection
|
||||
},
|
||||
},
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
struct MarkdownExample {
|
||||
markdown: View<Markdown>,
|
||||
}
|
||||
|
||||
impl MarkdownExample {
|
||||
pub fn new(
|
||||
text: String,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let markdown = cx.new_view(|cx| Markdown::new(text, style, language_registry, cx));
|
||||
Self { markdown }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MarkdownExample {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("markdown-example")
|
||||
.debug_selector(|| "foo".into())
|
||||
.relative()
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
.overflow_y_scroll()
|
||||
.child(self.markdown.clone())
|
||||
}
|
||||
}
|
||||
902
crates/markdown/src/markdown.rs
Normal file
902
crates/markdown/src/markdown.rs
Normal file
@@ -0,0 +1,902 @@
|
||||
mod parser;
|
||||
|
||||
use crate::parser::CodeBlockKind;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
point, quad, AnyElement, Bounds, CursorStyle, DispatchPhase, Edges, FontStyle, FontWeight,
|
||||
GlobalElementId, Hitbox, Hsla, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
|
||||
Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
|
||||
TextStyleRefinement, View,
|
||||
};
|
||||
use language::{Language, LanguageRegistry, Rope};
|
||||
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
|
||||
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MarkdownStyle {
|
||||
pub code_block: TextStyleRefinement,
|
||||
pub inline_code: TextStyleRefinement,
|
||||
pub block_quote: TextStyleRefinement,
|
||||
pub link: TextStyleRefinement,
|
||||
pub rule_color: Hsla,
|
||||
pub block_quote_border_color: Hsla,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
pub selection_background_color: Hsla,
|
||||
}
|
||||
|
||||
pub struct Markdown {
|
||||
source: String,
|
||||
selection: Selection,
|
||||
pressed_link: Option<RenderedLink>,
|
||||
autoscroll_request: Option<usize>,
|
||||
style: MarkdownStyle,
|
||||
parsed_markdown: ParsedMarkdown,
|
||||
should_reparse: bool,
|
||||
pending_parse: Option<Task<Option<()>>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
impl Markdown {
|
||||
pub fn new(
|
||||
source: String,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
source,
|
||||
selection: Selection::default(),
|
||||
pressed_link: None,
|
||||
autoscroll_request: None,
|
||||
style,
|
||||
should_reparse: false,
|
||||
parsed_markdown: ParsedMarkdown::default(),
|
||||
pending_parse: None,
|
||||
language_registry,
|
||||
};
|
||||
this.parse(cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn append(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
self.source.push_str(text);
|
||||
self.parse(cx);
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &str {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn parse(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.source.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.pending_parse.is_some() {
|
||||
self.should_reparse = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let text = self.source.clone();
|
||||
let parsed = cx.background_executor().spawn(async move {
|
||||
let text = SharedString::from(text);
|
||||
let events = Arc::from(parse_markdown(text.as_ref()));
|
||||
anyhow::Ok(ParsedMarkdown {
|
||||
source: text,
|
||||
events,
|
||||
})
|
||||
});
|
||||
|
||||
self.should_reparse = false;
|
||||
self.pending_parse = Some(cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let parsed = parsed.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.parsed_markdown = parsed;
|
||||
this.pending_parse.take();
|
||||
if this.should_reparse {
|
||||
this.parse(cx);
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Markdown {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
MarkdownElement::new(
|
||||
cx.view().clone(),
|
||||
self.style.clone(),
|
||||
self.language_registry.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
struct Selection {
|
||||
start: usize,
|
||||
end: usize,
|
||||
reversed: bool,
|
||||
pending: bool,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
fn set_head(&mut self, head: usize) {
|
||||
if head < self.tail() {
|
||||
if !self.reversed {
|
||||
self.end = self.start;
|
||||
self.reversed = true;
|
||||
}
|
||||
self.start = head;
|
||||
} else {
|
||||
if self.reversed {
|
||||
self.start = self.end;
|
||||
self.reversed = false;
|
||||
}
|
||||
self.end = head;
|
||||
}
|
||||
}
|
||||
|
||||
fn tail(&self) -> usize {
|
||||
if self.reversed {
|
||||
self.end
|
||||
} else {
|
||||
self.start
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ParsedMarkdown {
|
||||
source: SharedString,
|
||||
events: Arc<[(Range<usize>, MarkdownEvent)]>,
|
||||
}
|
||||
|
||||
impl Default for ParsedMarkdown {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
source: SharedString::default(),
|
||||
events: Arc::from([]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MarkdownElement {
|
||||
markdown: View<Markdown>,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
impl MarkdownElement {
|
||||
fn new(
|
||||
markdown: View<Markdown>,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
markdown,
|
||||
style,
|
||||
language_registry,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
|
||||
let language = self
|
||||
.language_registry
|
||||
.language_for_name(name)
|
||||
.map(|language| language.ok())
|
||||
.shared();
|
||||
|
||||
match language.clone().now_or_never() {
|
||||
Some(language) => language,
|
||||
None => {
|
||||
let markdown = self.markdown.downgrade();
|
||||
cx.spawn(|mut cx| async move {
|
||||
language.await;
|
||||
markdown.update(&mut cx, |_, cx| cx.notify())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_selection(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
rendered_text: &RenderedText,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let selection = self.markdown.read(cx).selection;
|
||||
let selection_start = rendered_text.position_for_source_index(selection.start);
|
||||
let selection_end = rendered_text.position_for_source_index(selection.end);
|
||||
|
||||
if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
|
||||
selection_start.zip(selection_end)
|
||||
{
|
||||
if start_position.y == end_position.y {
|
||||
cx.paint_quad(quad(
|
||||
Bounds::from_corners(
|
||||
start_position,
|
||||
point(end_position.x, end_position.y + end_line_height),
|
||||
),
|
||||
Pixels::ZERO,
|
||||
self.style.selection_background_color,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
} else {
|
||||
cx.paint_quad(quad(
|
||||
Bounds::from_corners(
|
||||
start_position,
|
||||
point(bounds.right(), start_position.y + start_line_height),
|
||||
),
|
||||
Pixels::ZERO,
|
||||
self.style.selection_background_color,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
|
||||
if end_position.y > start_position.y + start_line_height {
|
||||
cx.paint_quad(quad(
|
||||
Bounds::from_corners(
|
||||
point(bounds.left(), start_position.y + start_line_height),
|
||||
point(bounds.right(), end_position.y),
|
||||
),
|
||||
Pixels::ZERO,
|
||||
self.style.selection_background_color,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
}
|
||||
|
||||
cx.paint_quad(quad(
|
||||
Bounds::from_corners(
|
||||
point(bounds.left(), end_position.y),
|
||||
point(end_position.x, end_position.y + end_line_height),
|
||||
),
|
||||
Pixels::ZERO,
|
||||
self.style.selection_background_color,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_mouse_listeners(
|
||||
&mut self,
|
||||
hitbox: &Hitbox,
|
||||
rendered_text: &RenderedText,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let is_hovering_link = hitbox.is_hovered(cx)
|
||||
&& !self.markdown.read(cx).selection.pending
|
||||
&& rendered_text
|
||||
.link_for_position(cx.mouse_position())
|
||||
.is_some();
|
||||
|
||||
if is_hovering_link {
|
||||
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
} else {
|
||||
cx.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
}
|
||||
|
||||
self.on_mouse_event(cx, {
|
||||
let rendered_text = rendered_text.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
move |markdown, event: &MouseDownEvent, phase, cx| {
|
||||
if hitbox.is_hovered(cx) {
|
||||
if phase.bubble() {
|
||||
if let Some(link) = rendered_text.link_for_position(event.position) {
|
||||
markdown.pressed_link = Some(link.clone());
|
||||
} else {
|
||||
let source_index =
|
||||
match rendered_text.source_index_for_position(event.position) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
markdown.selection = Selection {
|
||||
start: source_index,
|
||||
end: source_index,
|
||||
reversed: false,
|
||||
pending: true,
|
||||
};
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
} else if phase.capture() {
|
||||
markdown.selection = Selection::default();
|
||||
markdown.pressed_link = None;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
self.on_mouse_event(cx, {
|
||||
let rendered_text = rendered_text.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
let was_hovering_link = is_hovering_link;
|
||||
move |markdown, event: &MouseMoveEvent, phase, cx| {
|
||||
if phase.capture() {
|
||||
return;
|
||||
}
|
||||
|
||||
if markdown.selection.pending {
|
||||
let source_index = match rendered_text.source_index_for_position(event.position)
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
markdown.selection.set_head(source_index);
|
||||
markdown.autoscroll_request = Some(source_index);
|
||||
cx.notify();
|
||||
} else {
|
||||
let is_hovering_link = hitbox.is_hovered(cx)
|
||||
&& rendered_text.link_for_position(event.position).is_some();
|
||||
if is_hovering_link != was_hovering_link {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.on_mouse_event(cx, {
|
||||
let rendered_text = rendered_text.clone();
|
||||
move |markdown, event: &MouseUpEvent, phase, cx| {
|
||||
if phase.bubble() {
|
||||
if let Some(pressed_link) = markdown.pressed_link.take() {
|
||||
if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
|
||||
cx.open_url(&pressed_link.destination_url);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if markdown.selection.pending {
|
||||
markdown.selection.pending = false;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn autoscroll(&mut self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> {
|
||||
let autoscroll_index = self
|
||||
.markdown
|
||||
.update(cx, |markdown, _| markdown.autoscroll_request.take())?;
|
||||
let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
|
||||
|
||||
let text_style = cx.text_style();
|
||||
let font_id = cx.text_system().resolve_font(&text_style.font());
|
||||
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||
let em_width = cx
|
||||
.text_system()
|
||||
.typographic_bounds(font_id, font_size, 'm')
|
||||
.unwrap()
|
||||
.size
|
||||
.width;
|
||||
cx.request_autoscroll(Bounds::from_corners(
|
||||
point(position.x - 3. * em_width, position.y - 3. * line_height),
|
||||
point(position.x + 3. * em_width, position.y + 3. * line_height),
|
||||
));
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn on_mouse_event<T: MouseEvent>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
mut f: impl 'static + FnMut(&mut Markdown, &T, DispatchPhase, &mut ViewContext<Markdown>),
|
||||
) {
|
||||
cx.on_mouse_event({
|
||||
let markdown = self.markdown.downgrade();
|
||||
move |event, phase, cx| {
|
||||
markdown
|
||||
.update(cx, |markdown, cx| f(markdown, event, phase, cx))
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for MarkdownElement {
|
||||
type RequestLayoutState = RenderedMarkdown;
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone());
|
||||
let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone();
|
||||
for (range, event) in parsed_markdown.events.iter() {
|
||||
match event {
|
||||
MarkdownEvent::Start(tag) => {
|
||||
match tag {
|
||||
MarkdownTag::Paragraph => {
|
||||
builder.push_div(div().mb_2().line_height(rems(1.3)));
|
||||
}
|
||||
MarkdownTag::Heading { level, .. } => {
|
||||
let mut heading = div().mb_2();
|
||||
heading = match level {
|
||||
pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
|
||||
pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
|
||||
pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
|
||||
pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
|
||||
_ => heading,
|
||||
};
|
||||
builder.push_div(heading);
|
||||
}
|
||||
MarkdownTag::BlockQuote => {
|
||||
builder.push_text_style(self.style.block_quote.clone());
|
||||
builder.push_div(
|
||||
div()
|
||||
.pl_4()
|
||||
.mb_2()
|
||||
.border_l_4()
|
||||
.border_color(self.style.block_quote_border_color),
|
||||
);
|
||||
}
|
||||
MarkdownTag::CodeBlock(kind) => {
|
||||
let language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
self.load_language(language.as_ref(), cx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
builder.push_code_block(language);
|
||||
builder.push_text_style(self.style.code_block.clone());
|
||||
builder.push_div(div().rounded_lg().p_4().mb_2().w_full().when_some(
|
||||
self.style.code_block.background_color,
|
||||
|div, color| div.bg(color),
|
||||
));
|
||||
}
|
||||
MarkdownTag::HtmlBlock => builder.push_div(div()),
|
||||
MarkdownTag::List(bullet_index) => {
|
||||
builder.push_list(*bullet_index);
|
||||
builder.push_div(div().pl_4());
|
||||
}
|
||||
MarkdownTag::Item => {
|
||||
let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
|
||||
format!("{}.", bullet_index)
|
||||
} else {
|
||||
"•".to_string()
|
||||
};
|
||||
builder.push_div(
|
||||
div()
|
||||
.h_flex()
|
||||
.mb_2()
|
||||
.line_height(rems(1.3))
|
||||
.items_start()
|
||||
.gap_1()
|
||||
.child(bullet),
|
||||
);
|
||||
// Without `w_0`, text doesn't wrap to the width of the container.
|
||||
builder.push_div(div().flex_1().w_0());
|
||||
}
|
||||
MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
|
||||
font_style: Some(FontStyle::Italic),
|
||||
..Default::default()
|
||||
}),
|
||||
MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}),
|
||||
MarkdownTag::Strikethrough => {
|
||||
builder.push_text_style(TextStyleRefinement {
|
||||
strikethrough: Some(StrikethroughStyle {
|
||||
thickness: px(1.),
|
||||
color: None,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
MarkdownTag::Link { dest_url, .. } => {
|
||||
builder.push_link(dest_url.clone(), range.clone());
|
||||
builder.push_text_style(self.style.link.clone())
|
||||
}
|
||||
_ => log::error!("unsupported markdown tag {:?}", tag),
|
||||
}
|
||||
}
|
||||
MarkdownEvent::End(tag) => match tag {
|
||||
MarkdownTagEnd::Paragraph => {
|
||||
builder.pop_div();
|
||||
}
|
||||
MarkdownTagEnd::Heading(_) => builder.pop_div(),
|
||||
MarkdownTagEnd::BlockQuote => {
|
||||
builder.pop_text_style();
|
||||
builder.pop_div()
|
||||
}
|
||||
MarkdownTagEnd::CodeBlock => {
|
||||
builder.trim_trailing_newline();
|
||||
builder.pop_div();
|
||||
builder.pop_text_style();
|
||||
builder.pop_code_block();
|
||||
}
|
||||
MarkdownTagEnd::HtmlBlock => builder.pop_div(),
|
||||
MarkdownTagEnd::List(_) => {
|
||||
builder.pop_list();
|
||||
builder.pop_div();
|
||||
}
|
||||
MarkdownTagEnd::Item => {
|
||||
builder.pop_div();
|
||||
builder.pop_div();
|
||||
}
|
||||
MarkdownTagEnd::Emphasis => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Strong => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Link => builder.pop_text_style(),
|
||||
_ => log::error!("unsupported markdown tag end: {:?}", tag),
|
||||
},
|
||||
MarkdownEvent::Text => {
|
||||
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
|
||||
}
|
||||
MarkdownEvent::Code => {
|
||||
builder.push_text_style(self.style.inline_code.clone());
|
||||
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
|
||||
builder.pop_text_style();
|
||||
}
|
||||
MarkdownEvent::Html => {
|
||||
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
|
||||
}
|
||||
MarkdownEvent::InlineHtml => {
|
||||
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
|
||||
}
|
||||
MarkdownEvent::Rule => {
|
||||
builder.push_div(
|
||||
div()
|
||||
.border_b_1()
|
||||
.my_2()
|
||||
.border_color(self.style.rule_color),
|
||||
);
|
||||
builder.pop_div()
|
||||
}
|
||||
MarkdownEvent::SoftBreak => builder.push_text("\n", range.start),
|
||||
MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
|
||||
_ => log::error!("unsupported markdown event {:?}", event),
|
||||
}
|
||||
}
|
||||
|
||||
let mut rendered_markdown = builder.build();
|
||||
let child_layout_id = rendered_markdown.element.request_layout(cx);
|
||||
let layout_id = cx.request_layout(Style::default(), [child_layout_id]);
|
||||
(layout_id, rendered_markdown)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
rendered_markdown: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self::PrepaintState {
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
rendered_markdown.element.prepaint(cx);
|
||||
self.autoscroll(&rendered_markdown.text, cx);
|
||||
hitbox
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
rendered_markdown: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.paint_mouse_listeners(hitbox, &rendered_markdown.text, cx);
|
||||
rendered_markdown.element.paint(cx);
|
||||
self.paint_selection(bounds, &rendered_markdown.text, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for MarkdownElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct MarkdownElementBuilder {
|
||||
div_stack: Vec<Div>,
|
||||
rendered_lines: Vec<RenderedLine>,
|
||||
pending_line: PendingLine,
|
||||
rendered_links: Vec<RenderedLink>,
|
||||
current_source_index: usize,
|
||||
base_text_style: TextStyle,
|
||||
text_style_stack: Vec<TextStyleRefinement>,
|
||||
code_block_stack: Vec<Option<Arc<Language>>>,
|
||||
list_stack: Vec<ListStackEntry>,
|
||||
syntax_theme: Arc<SyntaxTheme>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PendingLine {
|
||||
text: String,
|
||||
runs: Vec<TextRun>,
|
||||
source_mappings: Vec<SourceMapping>,
|
||||
}
|
||||
|
||||
struct ListStackEntry {
|
||||
bullet_index: Option<u64>,
|
||||
}
|
||||
|
||||
impl MarkdownElementBuilder {
|
||||
fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
|
||||
Self {
|
||||
div_stack: vec![div().debug_selector(|| "inner".into())],
|
||||
rendered_lines: Vec::new(),
|
||||
pending_line: PendingLine::default(),
|
||||
rendered_links: Vec::new(),
|
||||
current_source_index: 0,
|
||||
base_text_style,
|
||||
text_style_stack: Vec::new(),
|
||||
code_block_stack: Vec::new(),
|
||||
list_stack: Vec::new(),
|
||||
syntax_theme,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_text_style(&mut self, style: TextStyleRefinement) {
|
||||
self.text_style_stack.push(style);
|
||||
}
|
||||
|
||||
fn text_style(&self) -> TextStyle {
|
||||
let mut style = self.base_text_style.clone();
|
||||
for refinement in &self.text_style_stack {
|
||||
style.refine(refinement);
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
fn pop_text_style(&mut self) {
|
||||
self.text_style_stack.pop();
|
||||
}
|
||||
|
||||
fn push_div(&mut self, div: Div) {
|
||||
self.flush_text();
|
||||
self.div_stack.push(div);
|
||||
}
|
||||
|
||||
fn pop_div(&mut self) {
|
||||
self.flush_text();
|
||||
let div = self.div_stack.pop().unwrap().into_any();
|
||||
self.div_stack.last_mut().unwrap().extend(iter::once(div));
|
||||
}
|
||||
|
||||
fn push_list(&mut self, bullet_index: Option<u64>) {
|
||||
self.list_stack.push(ListStackEntry { bullet_index });
|
||||
}
|
||||
|
||||
fn next_bullet_index(&mut self) -> Option<u64> {
|
||||
self.list_stack.last_mut().and_then(|entry| {
|
||||
let item_index = entry.bullet_index.as_mut()?;
|
||||
*item_index += 1;
|
||||
Some(*item_index - 1)
|
||||
})
|
||||
}
|
||||
|
||||
fn pop_list(&mut self) {
|
||||
self.list_stack.pop();
|
||||
}
|
||||
|
||||
fn push_code_block(&mut self, language: Option<Arc<Language>>) {
|
||||
self.code_block_stack.push(language);
|
||||
}
|
||||
|
||||
fn pop_code_block(&mut self) {
|
||||
self.code_block_stack.pop();
|
||||
}
|
||||
|
||||
fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
|
||||
self.rendered_links.push(RenderedLink {
|
||||
source_range,
|
||||
destination_url,
|
||||
});
|
||||
}
|
||||
|
||||
fn push_text(&mut self, text: &str, source_index: usize) {
|
||||
self.pending_line.source_mappings.push(SourceMapping {
|
||||
rendered_index: self.pending_line.text.len(),
|
||||
source_index,
|
||||
});
|
||||
self.pending_line.text.push_str(text);
|
||||
self.current_source_index = source_index + text.len();
|
||||
|
||||
if let Some(Some(language)) = self.code_block_stack.last() {
|
||||
let mut offset = 0;
|
||||
for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
|
||||
if range.start > offset {
|
||||
self.pending_line
|
||||
.runs
|
||||
.push(self.text_style().to_run(range.start - offset));
|
||||
}
|
||||
|
||||
let mut run_style = self.text_style();
|
||||
if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
|
||||
run_style = run_style.highlight(highlight);
|
||||
}
|
||||
self.pending_line.runs.push(run_style.to_run(range.len()));
|
||||
offset = range.end;
|
||||
}
|
||||
|
||||
if offset < text.len() {
|
||||
self.pending_line
|
||||
.runs
|
||||
.push(self.text_style().to_run(text.len() - offset));
|
||||
}
|
||||
} else {
|
||||
self.pending_line
|
||||
.runs
|
||||
.push(self.text_style().to_run(text.len()));
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_trailing_newline(&mut self) {
|
||||
if self.pending_line.text.ends_with('\n') {
|
||||
self.pending_line
|
||||
.text
|
||||
.truncate(self.pending_line.text.len() - 1);
|
||||
self.pending_line.runs.last_mut().unwrap().len -= 1;
|
||||
self.current_source_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_text(&mut self) {
|
||||
let line = mem::take(&mut self.pending_line);
|
||||
if line.text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = StyledText::new(line.text).with_runs(line.runs);
|
||||
self.rendered_lines.push(RenderedLine {
|
||||
layout: text.layout().clone(),
|
||||
source_mappings: line.source_mappings,
|
||||
source_end: self.current_source_index,
|
||||
});
|
||||
self.div_stack.last_mut().unwrap().extend([text.into_any()]);
|
||||
}
|
||||
|
||||
fn build(mut self) -> RenderedMarkdown {
|
||||
debug_assert_eq!(self.div_stack.len(), 1);
|
||||
self.flush_text();
|
||||
RenderedMarkdown {
|
||||
element: self.div_stack.pop().unwrap().into_any(),
|
||||
text: RenderedText {
|
||||
lines: self.rendered_lines.into(),
|
||||
links: self.rendered_links.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderedLine {
|
||||
layout: TextLayout,
|
||||
source_mappings: Vec<SourceMapping>,
|
||||
source_end: usize,
|
||||
}
|
||||
|
||||
impl RenderedLine {
|
||||
fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
|
||||
let mapping = match self
|
||||
.source_mappings
|
||||
.binary_search_by_key(&source_index, |probe| probe.source_index)
|
||||
{
|
||||
Ok(ix) => &self.source_mappings[ix],
|
||||
Err(ix) => &self.source_mappings[ix - 1],
|
||||
};
|
||||
mapping.rendered_index + (source_index - mapping.source_index)
|
||||
}
|
||||
|
||||
fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
|
||||
let mapping = match self
|
||||
.source_mappings
|
||||
.binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
|
||||
{
|
||||
Ok(ix) => &self.source_mappings[ix],
|
||||
Err(ix) => &self.source_mappings[ix - 1],
|
||||
};
|
||||
mapping.source_index + (rendered_index - mapping.rendered_index)
|
||||
}
|
||||
|
||||
fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
|
||||
let line_rendered_index;
|
||||
let out_of_bounds;
|
||||
match self.layout.index_for_position(position) {
|
||||
Ok(ix) => {
|
||||
line_rendered_index = ix;
|
||||
out_of_bounds = false;
|
||||
}
|
||||
Err(ix) => {
|
||||
line_rendered_index = ix;
|
||||
out_of_bounds = true;
|
||||
}
|
||||
};
|
||||
let source_index = self.source_index_for_rendered_index(line_rendered_index);
|
||||
if out_of_bounds {
|
||||
Err(source_index)
|
||||
} else {
|
||||
Ok(source_index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
struct SourceMapping {
|
||||
rendered_index: usize,
|
||||
source_index: usize,
|
||||
}
|
||||
|
||||
pub struct RenderedMarkdown {
|
||||
element: AnyElement,
|
||||
text: RenderedText,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RenderedText {
|
||||
lines: Rc<[RenderedLine]>,
|
||||
links: Rc<[RenderedLink]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
struct RenderedLink {
|
||||
source_range: Range<usize>,
|
||||
destination_url: SharedString,
|
||||
}
|
||||
|
||||
impl RenderedText {
|
||||
fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
|
||||
let mut lines = self.lines.iter().peekable();
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
let line_bounds = line.layout.bounds();
|
||||
if position.y > line_bounds.bottom() {
|
||||
if let Some(next_line) = lines.peek() {
|
||||
if position.y < next_line.layout.bounds().top() {
|
||||
return Err(line.source_end);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return line.source_index_for_position(position);
|
||||
}
|
||||
|
||||
Err(self.lines.last().map_or(0, |line| line.source_end))
|
||||
}
|
||||
|
||||
fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
|
||||
for line in self.lines.iter() {
|
||||
let line_source_start = line.source_mappings.first().unwrap().source_index;
|
||||
if source_index < line_source_start {
|
||||
break;
|
||||
} else if source_index > line.source_end {
|
||||
continue;
|
||||
} else {
|
||||
let line_height = line.layout.line_height();
|
||||
let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
|
||||
let position = line.layout.position_for_index(rendered_index_within_line)?;
|
||||
return Some((position, line_height));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
|
||||
let source_index = self.source_index_for_position(position).ok()?;
|
||||
self.links
|
||||
.iter()
|
||||
.find(|link| link.source_range.contains(&source_index))
|
||||
}
|
||||
}
|
||||
274
crates/markdown/src/parser.rs
Normal file
274
crates/markdown/src/parser.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use gpui::SharedString;
|
||||
use linkify::LinkFinder;
|
||||
pub use pulldown_cmark::TagEnd as MarkdownTagEnd;
|
||||
use pulldown_cmark::{Alignment, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser};
|
||||
use std::ops::Range;
|
||||
|
||||
pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
|
||||
let mut events = Vec::new();
|
||||
let mut within_link = false;
|
||||
for (pulldown_event, mut range) in Parser::new_ext(text, Options::all()).into_offset_iter() {
|
||||
match pulldown_event {
|
||||
pulldown_cmark::Event::Start(tag) => {
|
||||
if let pulldown_cmark::Tag::Link { .. } = tag {
|
||||
within_link = true;
|
||||
}
|
||||
events.push((range, MarkdownEvent::Start(tag.into())))
|
||||
}
|
||||
pulldown_cmark::Event::End(tag) => {
|
||||
if let pulldown_cmark::TagEnd::Link = tag {
|
||||
within_link = false;
|
||||
}
|
||||
events.push((range, MarkdownEvent::End(tag)));
|
||||
}
|
||||
pulldown_cmark::Event::Text(_) => {
|
||||
// Automatically detect links in text if we're not already within a markdown
|
||||
// link.
|
||||
if !within_link {
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.kinds(&[linkify::LinkKind::Url]);
|
||||
let text_range = range.clone();
|
||||
for link in finder.links(&text[text_range.clone()]) {
|
||||
let link_range =
|
||||
text_range.start + link.start()..text_range.start + link.end();
|
||||
|
||||
if link_range.start > range.start {
|
||||
events.push((range.start..link_range.start, MarkdownEvent::Text));
|
||||
}
|
||||
|
||||
events.push((
|
||||
link_range.clone(),
|
||||
MarkdownEvent::Start(MarkdownTag::Link {
|
||||
link_type: LinkType::Autolink,
|
||||
dest_url: SharedString::from(link.as_str().to_string()),
|
||||
title: SharedString::default(),
|
||||
id: SharedString::default(),
|
||||
}),
|
||||
));
|
||||
events.push((link_range.clone(), MarkdownEvent::Text));
|
||||
events.push((link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link)));
|
||||
|
||||
range.start = link_range.end;
|
||||
}
|
||||
}
|
||||
|
||||
if range.start < range.end {
|
||||
events.push((range, MarkdownEvent::Text));
|
||||
}
|
||||
}
|
||||
pulldown_cmark::Event::Code(_) => {
|
||||
range.start += 1;
|
||||
range.end -= 1;
|
||||
events.push((range, MarkdownEvent::Code))
|
||||
}
|
||||
pulldown_cmark::Event::Html(_) => events.push((range, MarkdownEvent::Html)),
|
||||
pulldown_cmark::Event::InlineHtml(_) => events.push((range, MarkdownEvent::InlineHtml)),
|
||||
pulldown_cmark::Event::FootnoteReference(_) => {
|
||||
events.push((range, MarkdownEvent::FootnoteReference))
|
||||
}
|
||||
pulldown_cmark::Event::SoftBreak => events.push((range, MarkdownEvent::SoftBreak)),
|
||||
pulldown_cmark::Event::HardBreak => events.push((range, MarkdownEvent::HardBreak)),
|
||||
pulldown_cmark::Event::Rule => events.push((range, MarkdownEvent::Rule)),
|
||||
pulldown_cmark::Event::TaskListMarker(checked) => {
|
||||
events.push((range, MarkdownEvent::TaskListMarker(checked)))
|
||||
}
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
/// A static-lifetime equivalent of pulldown_cmark::Event so we can cache the
|
||||
/// parse result for rendering without resorting to unsafe lifetime coercion.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum MarkdownEvent {
|
||||
/// Start of a tagged element. Events that are yielded after this event
|
||||
/// and before its corresponding `End` event are inside this element.
|
||||
/// Start and end events are guaranteed to be balanced.
|
||||
Start(MarkdownTag),
|
||||
/// End of a tagged element.
|
||||
End(MarkdownTagEnd),
|
||||
/// A text node.
|
||||
Text,
|
||||
/// An inline code node.
|
||||
Code,
|
||||
/// An HTML node.
|
||||
Html,
|
||||
/// An inline HTML node.
|
||||
InlineHtml,
|
||||
/// A reference to a footnote with given label, which may or may not be defined
|
||||
/// by an event with a `Tag::FootnoteDefinition` tag. Definitions and references to them may
|
||||
/// occur in any order.
|
||||
FootnoteReference,
|
||||
/// A soft line break.
|
||||
SoftBreak,
|
||||
/// A hard line break.
|
||||
HardBreak,
|
||||
/// A horizontal ruler.
|
||||
Rule,
|
||||
/// A task list marker, rendered as a checkbox in HTML. Contains a true when it is checked.
|
||||
TaskListMarker(bool),
|
||||
}
|
||||
|
||||
/// Tags for elements that can contain other elements.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum MarkdownTag {
|
||||
/// A paragraph of text and other inline elements.
|
||||
Paragraph,
|
||||
|
||||
/// A heading, with optional identifier, classes and custom attributes.
|
||||
/// The identifier is prefixed with `#` and the last one in the attributes
|
||||
/// list is chosen, classes are prefixed with `.` and custom attributes
|
||||
/// have no prefix and can optionally have a value (`myattr` o `myattr=myvalue`).
|
||||
Heading {
|
||||
level: HeadingLevel,
|
||||
id: Option<SharedString>,
|
||||
classes: Vec<SharedString>,
|
||||
/// The first item of the tuple is the attr and second one the value.
|
||||
attrs: Vec<(SharedString, Option<SharedString>)>,
|
||||
},
|
||||
|
||||
BlockQuote,
|
||||
|
||||
/// A code block.
|
||||
CodeBlock(CodeBlockKind),
|
||||
|
||||
/// A HTML block.
|
||||
HtmlBlock,
|
||||
|
||||
/// A list. If the list is ordered the field indicates the number of the first item.
|
||||
/// Contains only list items.
|
||||
List(Option<u64>), // TODO: add delim and tight for ast (not needed for html)
|
||||
|
||||
/// A list item.
|
||||
Item,
|
||||
|
||||
/// A footnote definition. The value contained is the footnote's label by which it can
|
||||
/// be referred to.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
FootnoteDefinition(SharedString),
|
||||
|
||||
/// A table. Contains a vector describing the text-alignment for each of its columns.
|
||||
Table(Vec<Alignment>),
|
||||
|
||||
/// A table header. Contains only `TableCell`s. Note that the table body starts immediately
|
||||
/// after the closure of the `TableHead` tag. There is no `TableBody` tag.
|
||||
TableHead,
|
||||
|
||||
/// A table row. Is used both for header rows as body rows. Contains only `TableCell`s.
|
||||
TableRow,
|
||||
TableCell,
|
||||
|
||||
// span-level tags
|
||||
Emphasis,
|
||||
Strong,
|
||||
Strikethrough,
|
||||
|
||||
/// A link.
|
||||
Link {
|
||||
link_type: LinkType,
|
||||
dest_url: SharedString,
|
||||
title: SharedString,
|
||||
/// Identifier of reference links, e.g. `world` in the link `[hello][world]`.
|
||||
id: SharedString,
|
||||
},
|
||||
|
||||
/// An image. The first field is the link type, the second the destination URL and the third is a title,
|
||||
/// the fourth is the link identifier.
|
||||
Image {
|
||||
link_type: LinkType,
|
||||
dest_url: SharedString,
|
||||
title: SharedString,
|
||||
/// Identifier of reference links, e.g. `world` in the link `[hello][world]`.
|
||||
id: SharedString,
|
||||
},
|
||||
|
||||
/// A metadata block.
|
||||
MetadataBlock(MetadataBlockKind),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum CodeBlockKind {
|
||||
Indented,
|
||||
/// The value contained in the tag describes the language of the code, which may be empty.
|
||||
Fenced(SharedString),
|
||||
}
|
||||
|
||||
impl From<pulldown_cmark::Tag<'_>> for MarkdownTag {
|
||||
fn from(tag: pulldown_cmark::Tag) -> Self {
|
||||
match tag {
|
||||
pulldown_cmark::Tag::Paragraph => MarkdownTag::Paragraph,
|
||||
pulldown_cmark::Tag::Heading {
|
||||
level,
|
||||
id,
|
||||
classes,
|
||||
attrs,
|
||||
} => {
|
||||
let id = id.map(|id| SharedString::from(id.into_string()));
|
||||
let classes = classes
|
||||
.into_iter()
|
||||
.map(|c| SharedString::from(c.into_string()))
|
||||
.collect();
|
||||
let attrs = attrs
|
||||
.into_iter()
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
SharedString::from(key.into_string()),
|
||||
value.map(|v| SharedString::from(v.into_string())),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
MarkdownTag::Heading {
|
||||
level,
|
||||
id,
|
||||
classes,
|
||||
attrs,
|
||||
}
|
||||
}
|
||||
pulldown_cmark::Tag::BlockQuote => MarkdownTag::BlockQuote,
|
||||
pulldown_cmark::Tag::CodeBlock(kind) => match kind {
|
||||
pulldown_cmark::CodeBlockKind::Indented => {
|
||||
MarkdownTag::CodeBlock(CodeBlockKind::Indented)
|
||||
}
|
||||
pulldown_cmark::CodeBlockKind::Fenced(info) => MarkdownTag::CodeBlock(
|
||||
CodeBlockKind::Fenced(SharedString::from(info.into_string())),
|
||||
),
|
||||
},
|
||||
pulldown_cmark::Tag::List(start_number) => MarkdownTag::List(start_number),
|
||||
pulldown_cmark::Tag::Item => MarkdownTag::Item,
|
||||
pulldown_cmark::Tag::FootnoteDefinition(label) => {
|
||||
MarkdownTag::FootnoteDefinition(SharedString::from(label.to_string()))
|
||||
}
|
||||
pulldown_cmark::Tag::Table(alignments) => MarkdownTag::Table(alignments),
|
||||
pulldown_cmark::Tag::TableHead => MarkdownTag::TableHead,
|
||||
pulldown_cmark::Tag::TableRow => MarkdownTag::TableRow,
|
||||
pulldown_cmark::Tag::TableCell => MarkdownTag::TableCell,
|
||||
pulldown_cmark::Tag::Emphasis => MarkdownTag::Emphasis,
|
||||
pulldown_cmark::Tag::Strong => MarkdownTag::Strong,
|
||||
pulldown_cmark::Tag::Strikethrough => MarkdownTag::Strikethrough,
|
||||
pulldown_cmark::Tag::Link {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
} => MarkdownTag::Link {
|
||||
link_type,
|
||||
dest_url: SharedString::from(dest_url.into_string()),
|
||||
title: SharedString::from(title.into_string()),
|
||||
id: SharedString::from(id.into_string()),
|
||||
},
|
||||
pulldown_cmark::Tag::Image {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
} => MarkdownTag::Image {
|
||||
link_type,
|
||||
dest_url: SharedString::from(dest_url.into_string()),
|
||||
title: SharedString::from(title.into_string()),
|
||||
id: SharedString::from(id.into_string()),
|
||||
},
|
||||
pulldown_cmark::Tag::HtmlBlock => MarkdownTag::HtmlBlock,
|
||||
pulldown_cmark::Tag::MetadataBlock(kind) => MarkdownTag::MetadataBlock(kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use language::{
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape,
|
||||
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
|
||||
Outline, OutlineItem, Point, PointUtf16, Runnable, Selection, TextDimension, ToOffset as _,
|
||||
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
|
||||
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
@@ -1603,6 +1603,11 @@ impl MultiBuffer {
|
||||
"untitled".into()
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: String, cx: &mut ModelContext<Self>) {
|
||||
self.title = Some(title);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn is_parsing(&self, cx: &AppContext) -> bool {
|
||||
self.as_singleton().unwrap().read(cx).is_parsing()
|
||||
@@ -3151,10 +3156,10 @@ impl MultiBufferSnapshot {
|
||||
.redacted_ranges(excerpt.range.context.clone())
|
||||
.map(move |mut redacted_range| {
|
||||
// Re-base onto the excerpts coordinates in the multibuffer
|
||||
redacted_range.start =
|
||||
excerpt_offset + (redacted_range.start - excerpt_buffer_start);
|
||||
redacted_range.end =
|
||||
excerpt_offset + (redacted_range.end - excerpt_buffer_start);
|
||||
redacted_range.start = excerpt_offset
|
||||
+ redacted_range.start.saturating_sub(excerpt_buffer_start);
|
||||
redacted_range.end = excerpt_offset
|
||||
+ redacted_range.end.saturating_sub(excerpt_buffer_start);
|
||||
|
||||
redacted_range
|
||||
})
|
||||
@@ -3168,7 +3173,7 @@ impl MultiBufferSnapshot {
|
||||
pub fn runnable_ranges(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
|
||||
) -> impl Iterator<Item = language::RunnableRange> + '_ {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
self.excerpts_for_range(range.clone())
|
||||
.flat_map(move |(excerpt, excerpt_offset)| {
|
||||
@@ -3177,16 +3182,19 @@ impl MultiBufferSnapshot {
|
||||
excerpt
|
||||
.buffer
|
||||
.runnable_ranges(excerpt.range.context.clone())
|
||||
.map(move |(mut match_range, runnable)| {
|
||||
.map(move |mut runnable| {
|
||||
// Re-base onto the excerpts coordinates in the multibuffer
|
||||
match_range.start =
|
||||
excerpt_offset + (match_range.start - excerpt_buffer_start);
|
||||
match_range.end = excerpt_offset + (match_range.end - excerpt_buffer_start);
|
||||
|
||||
(match_range, runnable)
|
||||
runnable.run_range.start = excerpt_offset
|
||||
+ runnable
|
||||
.run_range
|
||||
.start
|
||||
.saturating_sub(excerpt_buffer_start);
|
||||
runnable.run_range.end = excerpt_offset
|
||||
+ runnable.run_range.end.saturating_sub(excerpt_buffer_start);
|
||||
runnable
|
||||
})
|
||||
.skip_while(move |(match_range, _)| match_range.end < range.start)
|
||||
.take_while(move |(match_range, _)| match_range.start < range.end)
|
||||
.skip_while(move |runnable| runnable.run_range.end < range.start)
|
||||
.take_while(move |runnable| runnable.run_range.start < range.end)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,15 +12,28 @@ workspace = true
|
||||
path = "src/node_runtime.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["tempfile"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
async_zip.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
walkdir = "2.5.0"
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
118
crates/node_runtime/src/archive.rs
Normal file
118
crates/node_runtime/src/archive.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_zip::base::read::stream::ZipFileReader;
|
||||
use futures::{io::BufReader, AsyncRead};
|
||||
|
||||
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
|
||||
let mut reader = ZipFileReader::new(BufReader::new(reader));
|
||||
|
||||
let destination = &destination
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| destination.to_path_buf());
|
||||
|
||||
while let Some(mut item) = reader.next_with_entry().await? {
|
||||
let entry_reader = item.reader_mut();
|
||||
let entry = entry_reader.entry();
|
||||
let path = destination.join(entry.filename().as_str().unwrap());
|
||||
|
||||
if entry.dir().unwrap() {
|
||||
std::fs::create_dir_all(&path)?;
|
||||
} else {
|
||||
let parent_dir = path.parent().expect("failed to get parent directory");
|
||||
std::fs::create_dir_all(&parent_dir)?;
|
||||
let mut file = smol::fs::File::create(&path).await?;
|
||||
futures::io::copy(entry_reader, &mut file).await?;
|
||||
}
|
||||
|
||||
reader = item.skip().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use async_zip::base::write::ZipFileWriter;
|
||||
use async_zip::ZipEntryBuilder;
|
||||
use futures::AsyncWriteExt;
|
||||
use smol::io::Cursor;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {
|
||||
let mut out = smol::fs::File::create(dst).await?;
|
||||
let mut writer = ZipFileWriter::new(&mut out);
|
||||
|
||||
for entry in walkdir::WalkDir::new(src_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let relative_path = path.strip_prefix(src_dir)?;
|
||||
let data = smol::fs::read(&path).await?;
|
||||
|
||||
let filename = relative_path.display().to_string();
|
||||
let builder = ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
|
||||
|
||||
writer.write_entry_whole(builder, &data).await?;
|
||||
}
|
||||
|
||||
writer.close().await?;
|
||||
out.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_file_content(path: &Path, content: &str) {
|
||||
assert!(path.exists(), "file not found: {:?}", path);
|
||||
let actual = std::fs::read_to_string(path).unwrap();
|
||||
assert_eq!(actual, content);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn make_test_data() -> TempDir {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dst = dir.path();
|
||||
|
||||
std::fs::write(&dst.join("test"), "Hello world.").unwrap();
|
||||
std::fs::create_dir_all(&dst.join("foo/bar")).unwrap();
|
||||
std::fs::write(&dst.join("foo/bar.txt"), "Foo bar.").unwrap();
|
||||
std::fs::write(&dst.join("foo/dar.md"), "Bar dar.").unwrap();
|
||||
std::fs::write(&dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
|
||||
|
||||
dir
|
||||
}
|
||||
|
||||
async fn read_archive(path: &PathBuf) -> impl AsyncRead + Unpin {
|
||||
let data = smol::fs::read(&path).await.unwrap();
|
||||
Cursor::new(data)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_zip() {
|
||||
let test_dir = make_test_data();
|
||||
let zip_file = test_dir.path().join("test.zip");
|
||||
|
||||
smol::block_on(async {
|
||||
compress_zip(&test_dir.path(), &zip_file).await.unwrap();
|
||||
let reader = read_archive(&zip_file).await;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dst = dir.path();
|
||||
extract_zip(dst, reader).await.unwrap();
|
||||
|
||||
assert_file_content(&dst.join("test"), "Hello world.");
|
||||
assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
|
||||
assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
|
||||
assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
mod archive;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use futures::AsyncReadExt;
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use smol::{fs, io::BufReader, lock::Mutex, process::Command};
|
||||
use smol::io::BufReader;
|
||||
use smol::{fs, lock::Mutex, process::Command};
|
||||
use std::io;
|
||||
use std::process::{Output, Stdio};
|
||||
use std::{
|
||||
@@ -15,8 +18,26 @@ use std::{
|
||||
use util::http::HttpClient;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg(windows)]
|
||||
use smol::process::windows::CommandExt;
|
||||
|
||||
const VERSION: &str = "v18.15.0";
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const NODE_PATH: &str = "bin/node";
|
||||
#[cfg(windows)]
|
||||
const NODE_PATH: &str = "node.exe";
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const NPM_PATH: &str = "bin/npm";
|
||||
#[cfg(windows)]
|
||||
const NPM_PATH: &str = "node_modules/npm/bin/npm-cli.js";
|
||||
|
||||
enum ArchiveType {
|
||||
TarGz,
|
||||
Zip,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct NpmInfo {
|
||||
@@ -119,10 +140,12 @@ impl RealNodeRuntime {
|
||||
let folder_name = format!("node-{VERSION}-{os}-{arch}");
|
||||
let node_containing_dir = util::paths::SUPPORT_DIR.join("node");
|
||||
let node_dir = node_containing_dir.join(folder_name);
|
||||
let node_binary = node_dir.join("bin/node");
|
||||
let npm_file = node_dir.join("bin/npm");
|
||||
let node_binary = node_dir.join(NODE_PATH);
|
||||
let npm_file = node_dir.join(NPM_PATH);
|
||||
|
||||
let result = Command::new(&node_binary)
|
||||
let mut command = Command::new(&node_binary);
|
||||
|
||||
command
|
||||
.env_clear()
|
||||
.arg(npm_file)
|
||||
.arg("--version")
|
||||
@@ -131,9 +154,12 @@ impl RealNodeRuntime {
|
||||
.stderr(Stdio::null())
|
||||
.args(["--cache".into(), node_dir.join("cache")])
|
||||
.args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
|
||||
.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")])
|
||||
.status()
|
||||
.await;
|
||||
.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]);
|
||||
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
|
||||
let result = command.status().await;
|
||||
let valid = matches!(result, Ok(status) if status.success());
|
||||
|
||||
if !valid {
|
||||
@@ -142,7 +168,19 @@ impl RealNodeRuntime {
|
||||
.await
|
||||
.context("error creating node containing dir")?;
|
||||
|
||||
let file_name = format!("node-{VERSION}-{os}-{arch}.tar.gz");
|
||||
let archive_type = match consts::OS {
|
||||
"macos" | "linux" => ArchiveType::TarGz,
|
||||
"windows" => ArchiveType::Zip,
|
||||
other => bail!("Running on unsupported os: {other}"),
|
||||
};
|
||||
|
||||
let file_name = format!(
|
||||
"node-{VERSION}-{os}-{arch}.{extension}",
|
||||
extension = match archive_type {
|
||||
ArchiveType::TarGz => "tar.gz",
|
||||
ArchiveType::Zip => "zip",
|
||||
}
|
||||
);
|
||||
let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
|
||||
let mut response = self
|
||||
.http
|
||||
@@ -150,9 +188,15 @@ impl RealNodeRuntime {
|
||||
.await
|
||||
.context("error downloading Node binary tarball")?;
|
||||
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(&node_containing_dir).await?;
|
||||
let body = response.body_mut();
|
||||
match archive_type {
|
||||
ArchiveType::TarGz => {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(&node_containing_dir).await?;
|
||||
}
|
||||
ArchiveType::Zip => archive::extract_zip(&node_containing_dir, body).await?,
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Not in the `if !valid {}` so we can populate these for existing installations
|
||||
@@ -168,7 +212,7 @@ impl RealNodeRuntime {
|
||||
impl NodeRuntime for RealNodeRuntime {
|
||||
async fn binary_path(&self) -> Result<PathBuf> {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
Ok(installation_path.join("bin/node"))
|
||||
Ok(installation_path.join(NODE_PATH))
|
||||
}
|
||||
|
||||
async fn run_npm_subcommand(
|
||||
@@ -180,7 +224,13 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
let attempt = || async move {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
|
||||
let mut env_path = installation_path.join("bin").into_os_string();
|
||||
let node_binary = installation_path.join(NODE_PATH);
|
||||
let npm_file = installation_path.join(NPM_PATH);
|
||||
let mut env_path = node_binary
|
||||
.parent()
|
||||
.expect("invalid node binary path")
|
||||
.to_path_buf();
|
||||
|
||||
if let Some(existing_path) = std::env::var_os("PATH") {
|
||||
if !existing_path.is_empty() {
|
||||
env_path.push(":");
|
||||
@@ -188,9 +238,6 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
|
||||
if smol::fs::metadata(&node_binary).await.is_err() {
|
||||
return Err(anyhow!("missing node binary file"));
|
||||
}
|
||||
@@ -219,6 +266,9 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
command.args(["--prefix".into(), directory.to_path_buf()]);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
|
||||
command.output().await.map_err(|e| anyhow!("{e}"))
|
||||
};
|
||||
|
||||
@@ -227,7 +277,8 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
output = attempt().await;
|
||||
if output.is_err() {
|
||||
return Err(anyhow!(
|
||||
"failed to launch npm subcommand {subcommand} subcommand"
|
||||
"failed to launch npm subcommand {subcommand} subcommand\nerr: {:?}",
|
||||
output.err()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7864,6 +7864,18 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn project_path_for_absolute_path(
|
||||
&self,
|
||||
abs_path: &Path,
|
||||
cx: &AppContext,
|
||||
) -> Option<ProjectPath> {
|
||||
self.find_local_worktree(abs_path, cx)
|
||||
.map(|(worktree, relative_path)| ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: relative_path.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_workspace_root(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
|
||||
@@ -250,6 +250,7 @@ impl SearchQuery {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
&self,
|
||||
buffer: &BufferSnapshot,
|
||||
|
||||
@@ -10,4 +10,4 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
once_cell = "1.19.0"
|
||||
once_cell.workspace = true
|
||||
|
||||
@@ -7,7 +7,8 @@ use std::{env, str::FromStr};
|
||||
use gpui::{AppContext, Global, SemanticVersion};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
|
||||
/// stable | dev | nightly | preview
|
||||
pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
|
||||
Lazy::new(|| {
|
||||
env::var("ZED_RELEASE_CHANNEL")
|
||||
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
|
||||
|
||||
@@ -450,7 +450,7 @@ pub struct WorktreeSearchResult {
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Status {
|
||||
Idle,
|
||||
Loading,
|
||||
|
||||
@@ -67,7 +67,7 @@ impl StoryContainer {
|
||||
}
|
||||
|
||||
impl ParentElement for StoryContainer {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
@@ -372,7 +372,7 @@ impl RenderOnce for StorySection {
|
||||
}
|
||||
|
||||
impl ParentElement for StorySection {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ collections.workspace = true
|
||||
dirs = "4.0.0"
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
libc = "0.2"
|
||||
libc.workspace = true
|
||||
task.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -559,7 +559,7 @@ impl Element for TerminalElement {
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = relative(1.).into();
|
||||
let layout_id = cx.request_layout(&style, None);
|
||||
let layout_id = cx.request_layout(style, None);
|
||||
|
||||
layout_id
|
||||
});
|
||||
|
||||
@@ -74,6 +74,7 @@ impl TerminalPanel {
|
||||
pane.set_can_split(false, cx);
|
||||
pane.set_can_navigate(false, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_| true);
|
||||
pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -407,7 +407,7 @@ impl VisibleOnHover for ButtonLike {
|
||||
}
|
||||
|
||||
impl ParentElement for ButtonLike {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ impl LabelCommon for LabelLike {
|
||||
}
|
||||
|
||||
impl ParentElement for LabelLike {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl List {
|
||||
}
|
||||
|
||||
impl ParentElement for List {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ impl Selectable for ListItem {
|
||||
}
|
||||
|
||||
impl ParentElement for ListItem {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ impl ModalHeader {
|
||||
}
|
||||
|
||||
impl ParentElement for ModalHeader {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ impl ModalContent {
|
||||
}
|
||||
|
||||
impl ParentElement for ModalContent {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ impl ModalRow {
|
||||
}
|
||||
|
||||
impl ParentElement for ModalRow {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ impl Popover {
|
||||
}
|
||||
|
||||
impl ParentElement for Popover {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
|
||||
.map(|child_element| child_element.request_layout(cx));
|
||||
|
||||
let layout_id = cx.request_layout(
|
||||
&gpui::Style::default(),
|
||||
gpui::Style::default(),
|
||||
menu_layout_id.into_iter().chain(child_layout_id),
|
||||
);
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
|
||||
.map(|child_element| child_element.request_layout(cx));
|
||||
|
||||
let layout_id = cx.request_layout(
|
||||
&gpui::Style::default(),
|
||||
gpui::Style::default(),
|
||||
menu_layout_id.into_iter().chain(child_layout_id),
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user