Compare commits
231 Commits
linux-sche
...
steps-cont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a75936419 | ||
|
|
4c7f1032a4 | ||
|
|
d61eaea4b9 | ||
|
|
f5d50f2b1e | ||
|
|
be4b19babb | ||
|
|
272be98ec8 | ||
|
|
e3d5eff858 | ||
|
|
9cb17ac630 | ||
|
|
fb541accb2 | ||
|
|
836f623800 | ||
|
|
18c2e8f6ca | ||
|
|
bf4645b1fe | ||
|
|
4d177918c1 | ||
|
|
87457f9ae8 | ||
|
|
be45f32753 | ||
|
|
18b5a87298 | ||
|
|
48211e8ce2 | ||
|
|
5008a388e6 | ||
|
|
3c417864e6 | ||
|
|
ad3055076d | ||
|
|
7c63f26aa9 | ||
|
|
f15a441c9d | ||
|
|
cb217381ce | ||
|
|
862f5a0561 | ||
|
|
7d30175527 | ||
|
|
d044685706 | ||
|
|
0496d0db9a | ||
|
|
c19e71f51e | ||
|
|
f7ff9b0811 | ||
|
|
be1ff8aee9 | ||
|
|
3a83fecea9 | ||
|
|
bac4a0428d | ||
|
|
ed3d3dc690 | ||
|
|
24d9374744 | ||
|
|
5d751f232c | ||
|
|
3fb3148995 | ||
|
|
76ce1a8f50 | ||
|
|
80558a3986 | ||
|
|
cdfadcc582 | ||
|
|
013c9f0420 | ||
|
|
22a2cc6950 | ||
|
|
49effeb7ba | ||
|
|
edda634ca5 | ||
|
|
75948e536f | ||
|
|
eb7fe57453 | ||
|
|
6e08e49d30 | ||
|
|
2c8ead4423 | ||
|
|
ca2976559e | ||
|
|
457da7b27c | ||
|
|
a2424638f5 | ||
|
|
ba4fa17b83 | ||
|
|
f5f4578422 | ||
|
|
75775292b3 | ||
|
|
344e315174 | ||
|
|
8ef53aafa2 | ||
|
|
00c3c02f7d | ||
|
|
2edf224599 | ||
|
|
16a4c59be0 | ||
|
|
90a46b0463 | ||
|
|
09b216cf5e | ||
|
|
84b34677e2 | ||
|
|
5a090bc3f3 | ||
|
|
4852e170ff | ||
|
|
53bcc3649a | ||
|
|
9241b11e1f | ||
|
|
8e9e94de22 | ||
|
|
1607e4e0e1 | ||
|
|
ceffc7e1cf | ||
|
|
e69b995a3b | ||
|
|
f19b51c0b2 | ||
|
|
1d2d3b209e | ||
|
|
959fbd5e45 | ||
|
|
ff85f1d608 | ||
|
|
d1300b7a66 | ||
|
|
62f5503e93 | ||
|
|
738d079aa7 | ||
|
|
4feb994ad8 | ||
|
|
40ed3b6977 | ||
|
|
bd02f4fe28 | ||
|
|
85bc233920 | ||
|
|
33f68882c1 | ||
|
|
acc9c2421b | ||
|
|
2cdfae9ce3 | ||
|
|
cf92b83c04 | ||
|
|
252737aef4 | ||
|
|
9c43450fef | ||
|
|
f8cfb50bb4 | ||
|
|
9499adf50d | ||
|
|
696591ca55 | ||
|
|
f9b0792aa0 | ||
|
|
60b22cf029 | ||
|
|
1fe16f42ea | ||
|
|
448ef538b3 | ||
|
|
f612c40bee | ||
|
|
0c6105992c | ||
|
|
64a796d436 | ||
|
|
76594ae5cd | ||
|
|
05de1dc8d2 | ||
|
|
ef5305869f | ||
|
|
8028e7f1b6 | ||
|
|
cb6fc11abc | ||
|
|
cf8bd4a90a | ||
|
|
62ab6e1a11 | ||
|
|
be1387fee6 | ||
|
|
09459fa3a4 | ||
|
|
400ae9c650 | ||
|
|
f9472ce90b | ||
|
|
0556eddc21 | ||
|
|
09c497f744 | ||
|
|
1b85438df9 | ||
|
|
b3d0ac3e3c | ||
|
|
a51a9b0354 | ||
|
|
6cf6614c48 | ||
|
|
0bf0152b74 | ||
|
|
d338e4a8a6 | ||
|
|
751508b6a2 | ||
|
|
f54f5dff65 | ||
|
|
66f0c390a8 | ||
|
|
fdd233eea9 | ||
|
|
bf7e474bbc | ||
|
|
2a8cee57c9 | ||
|
|
ef20afa9a4 | ||
|
|
e413823ae7 | ||
|
|
e68d9f4625 | ||
|
|
3407256aa3 | ||
|
|
abc5abcd8b | ||
|
|
da33aac156 | ||
|
|
1818fef32f | ||
|
|
2ae1a472e4 | ||
|
|
d7a25c1696 | ||
|
|
2c6cb4ec16 | ||
|
|
143035b1ed | ||
|
|
fa3d29087d | ||
|
|
1856320516 | ||
|
|
b58abb171f | ||
|
|
868455f978 | ||
|
|
c27c41274a | ||
|
|
0b0de8ca83 | ||
|
|
f3ddd18201 | ||
|
|
5d860e2286 | ||
|
|
e26dbe2c5b | ||
|
|
3c38be59b5 | ||
|
|
e50811c425 | ||
|
|
c1aa4d939c | ||
|
|
e8d674dc04 | ||
|
|
f0279e672a | ||
|
|
98b95d9a51 | ||
|
|
decdd3b6ac | ||
|
|
977a1b7a82 | ||
|
|
684d9dde56 | ||
|
|
315692d112 | ||
|
|
ba09eabfba | ||
|
|
70d983abe3 | ||
|
|
4a3097d4dd | ||
|
|
59ce3535d3 | ||
|
|
f8b5e42070 | ||
|
|
88c5eb550e | ||
|
|
e5dc6beace | ||
|
|
3a410942b4 | ||
|
|
89fbd6528f | ||
|
|
9ce989a704 | ||
|
|
dd63e25f23 | ||
|
|
489077befc | ||
|
|
21c5ce2bbd | ||
|
|
3deb000f70 | ||
|
|
fe3fe945a9 | ||
|
|
11178eacc7 | ||
|
|
59bc027750 | ||
|
|
0a718c65e2 | ||
|
|
85d77a3eec | ||
|
|
ca80343486 | ||
|
|
739038ddaf | ||
|
|
106e0623dd | ||
|
|
607ad6de3c | ||
|
|
ea26a01f5f | ||
|
|
8abc000553 | ||
|
|
9f5309cedd | ||
|
|
adf74fdc14 | ||
|
|
e402d7e96a | ||
|
|
12dfd4a2c2 | ||
|
|
b87d1eabcc | ||
|
|
ac528dda64 | ||
|
|
906688f012 | ||
|
|
c18e9aedcd | ||
|
|
cd4847ca22 | ||
|
|
4c63e8b203 | ||
|
|
d9d8c1f6d9 | ||
|
|
b0dbc80575 | ||
|
|
8e853e2b56 | ||
|
|
47a78907d6 | ||
|
|
0c1a3db87d | ||
|
|
3541a1175f | ||
|
|
e51d469025 | ||
|
|
018a2a29ea | ||
|
|
d49727ff10 | ||
|
|
c195c4ddff | ||
|
|
fd03454540 | ||
|
|
6eeec9b403 | ||
|
|
b558e8da1e | ||
|
|
1d7b28c658 | ||
|
|
de78eb44b1 | ||
|
|
c071e19899 | ||
|
|
37fc4ce09d | ||
|
|
99f56252be | ||
|
|
f61abe0247 | ||
|
|
45c54d189a | ||
|
|
2727f55772 | ||
|
|
291d64c803 | ||
|
|
6a11184ea3 | ||
|
|
ff1dcff2fb | ||
|
|
bef2586eed | ||
|
|
bdba8b23fa | ||
|
|
22900554d5 | ||
|
|
6db0b6c5ad | ||
|
|
ba11e9a9a8 | ||
|
|
de570133ff | ||
|
|
f1b1a9fd5e | ||
|
|
1b08f14c54 | ||
|
|
36d3b16279 | ||
|
|
945764e409 | ||
|
|
f1281c14dd | ||
|
|
3b823d4a0b | ||
|
|
46645b552f | ||
|
|
5bc3846d59 | ||
|
|
e6d608fa05 | ||
|
|
e106a39620 | ||
|
|
d32e9f759c | ||
|
|
15662f105e | ||
|
|
1887a6db53 | ||
|
|
fa9360f78d | ||
|
|
3ff738fa03 |
28
.git-blame-ignore-revs
Normal file
28
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,28 @@
|
||||
# .git-blame-ignore-revs
|
||||
#
|
||||
# This file consists of a list of commits that should be ignored for
|
||||
# `git blame` purposes. This is useful for ignoring commits that only
|
||||
# changed whitespace / indentation / formatting, but did not change
|
||||
# the underlying syntax tree.
|
||||
#
|
||||
# GitHub will pick this up automatically for blame views:
|
||||
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
|
||||
# To use this file locally, run:
|
||||
# git blame --ignore-revs-file .git-blame-ignore-revs
|
||||
# To always use this file by default, run:
|
||||
# git config --local blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
# To disable this functionality, run:
|
||||
# git config --local blame.ignoreRevsFile ""
|
||||
# Comments are optional, but may provide helpful context.
|
||||
|
||||
# 2023-04-20 Set default tab_size for JSON to 2 and apply new formatting
|
||||
# https://github.com/zed-industries/zed/pull/2394
|
||||
eca93c124a488b4e538946cd2d313bd571aa2b86
|
||||
|
||||
# 2024-02-25 Format JSON files in assets/
|
||||
# https://github.com/zed-industries/zed/pull/8405
|
||||
ffdda588b41f7d9d270ffe76cab116f828ad545e
|
||||
|
||||
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
|
||||
# https://github.com/zed-industries/zed/pull/13887
|
||||
813cc3f5e537372fc86720b5e71b6e1c815440ab
|
||||
3
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
3
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
@@ -27,7 +27,8 @@ body:
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
macOS: `~/Library/Logs/Zed/Zed.log`
|
||||
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary><pre>
|
||||
|
||||
57
.github/workflows/release_nightly.yml
vendored
57
.github/workflows/release_nightly.yml
vendored
@@ -93,9 +93,9 @@ jobs:
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly macos
|
||||
|
||||
bundle-deb:
|
||||
bundle-linux-x86:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux *.tar.gz bundle
|
||||
name: Create a Linux *.tar.gz bundle for x86
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -121,8 +121,57 @@ jobs:
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
- name: Create Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-targz
|
||||
|
||||
bundle-linux-arm:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux *.tar.gz bundle for ARM
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- hosted-linux-arm-1
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: "Setup jq"
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
|
||||
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: rui314/setup-mold@v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
- name: rustup
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Create Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
278
Cargo.lock
generated
278
Cargo.lock
generated
@@ -87,7 +87,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -110,7 +110,7 @@ version = "0.24.1-dev"
|
||||
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -341,8 +341,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a"
|
||||
dependencies = [
|
||||
"async-fs 2.1.1",
|
||||
"async-net 2.0.0",
|
||||
@@ -371,6 +372,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anthropic",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"assistant_slash_command",
|
||||
"async-watch",
|
||||
"breadcrumbs",
|
||||
@@ -407,6 +409,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rope",
|
||||
"roxmltree 0.20.0",
|
||||
"schemars",
|
||||
"search",
|
||||
"semantic_index",
|
||||
@@ -415,7 +418,6 @@ dependencies = [
|
||||
"settings",
|
||||
"similar",
|
||||
"smol",
|
||||
"strsim 0.11.1",
|
||||
"strum",
|
||||
"telemetry_events",
|
||||
"terminal",
|
||||
@@ -1583,7 +1585,16 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
"bit-vec 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f"
|
||||
dependencies = [
|
||||
"bit-vec 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1592,6 +1603,12 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
@@ -1606,9 +1623,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.2"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1634,11 +1651,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
|
||||
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"block",
|
||||
"bytemuck",
|
||||
"codespan-reporting",
|
||||
@@ -1664,7 +1681,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
|
||||
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1674,7 +1691,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
|
||||
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
@@ -1922,7 +1939,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"log",
|
||||
"polling 3.3.2",
|
||||
"rustix 0.38.32",
|
||||
@@ -2228,7 +2245,7 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex 0.2.4",
|
||||
"indexmap 1.9.3",
|
||||
"strsim 0.10.0",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
@@ -2252,7 +2269,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex 0.5.1",
|
||||
"strsim 0.10.0",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2391,16 +2408,6 @@ dependencies = [
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
|
||||
dependencies = [
|
||||
"lazy-bytes-cast",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clock"
|
||||
version = "0.1.0"
|
||||
@@ -2558,6 +2565,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"call",
|
||||
"channel",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"db",
|
||||
@@ -2859,7 +2867,7 @@ name = "cosmic-text"
|
||||
version = "0.11.2"
|
||||
source = "git+https://github.com/pop-os/cosmic-text?rev=542b20c#542b20ca4376a3b5de5fa629db1a4ace44e18e0c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"fontdb",
|
||||
"log",
|
||||
"rangemap",
|
||||
@@ -3396,11 +3404,13 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feature_flags",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"multi_buffer",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
@@ -3585,6 +3595,7 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
@@ -3593,6 +3604,7 @@ dependencies = [
|
||||
"db",
|
||||
"emojis",
|
||||
"env_logger",
|
||||
"file_icons",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"git",
|
||||
@@ -3605,6 +3617,7 @@ dependencies = [
|
||||
"linkify",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"multi_buffer",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot",
|
||||
@@ -3978,6 +3991,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"extension",
|
||||
@@ -3997,6 +4011,7 @@ dependencies = [
|
||||
"theme_selector",
|
||||
"ui",
|
||||
"util",
|
||||
"vim",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -4012,7 +4027,7 @@ version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bit-set 0.5.3",
|
||||
"regex",
|
||||
]
|
||||
|
||||
@@ -4069,7 +4084,7 @@ name = "feedback"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"client",
|
||||
"db",
|
||||
"editor",
|
||||
@@ -4258,7 +4273,7 @@ version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d"
|
||||
dependencies = [
|
||||
"roxmltree",
|
||||
"roxmltree 0.19.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4269,7 +4284,7 @@ checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
|
||||
dependencies = [
|
||||
"fontconfig-parser",
|
||||
"log",
|
||||
"memmap2 0.9.4",
|
||||
"memmap2",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser",
|
||||
@@ -4403,7 +4418,7 @@ dependencies = [
|
||||
name = "fsevent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"fsevent-sys 3.1.0",
|
||||
"parking_lot",
|
||||
@@ -4714,7 +4729,7 @@ version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
@@ -4825,7 +4840,7 @@ version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"gpu-alloc-types",
|
||||
]
|
||||
|
||||
@@ -4846,7 +4861,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4867,7 +4882,6 @@ dependencies = [
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cbindgen",
|
||||
"clipboard-win",
|
||||
"cocoa",
|
||||
"collections",
|
||||
"core-foundation",
|
||||
@@ -4896,6 +4910,7 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"objc",
|
||||
"oo7",
|
||||
"open",
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
@@ -5058,6 +5073,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"extension",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
@@ -5103,7 +5119,7 @@ version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f7acb9683d7c7068aa46d47557bfa4e35a277964b350d9504a87b03610163fd"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"byteorder",
|
||||
"heed-traits",
|
||||
"heed-types",
|
||||
@@ -5481,7 +5497,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"project",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -5707,6 +5722,25 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isahc"
|
||||
version = "1.7.2"
|
||||
@@ -5911,6 +5945,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"async-watch",
|
||||
"clock",
|
||||
"collections",
|
||||
"ctor",
|
||||
@@ -6052,12 +6087,6 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-bytes-cast"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@@ -6524,15 +6553,6 @@ dependencies = [
|
||||
"rustix 0.38.32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.4"
|
||||
@@ -6666,17 +6686,17 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e"
|
||||
version = "0.20.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=425526828f738c95ec50b016c6a761bc00d2fb25#425526828f738c95ec50b016c6a761bc00d2fb25"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bitflags 2.4.2",
|
||||
"arrayvec",
|
||||
"bit-set 0.6.0",
|
||||
"bitflags 2.6.0",
|
||||
"cfg_aliases",
|
||||
"codespan-reporting",
|
||||
"hexf-parse",
|
||||
"indexmap 2.2.6",
|
||||
"log",
|
||||
"num-traits",
|
||||
"rustc-hash",
|
||||
"spirv",
|
||||
"termcolor",
|
||||
@@ -6772,7 +6792,7 @@ version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset",
|
||||
@@ -6784,7 +6804,7 @@ version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -6853,7 +6873,7 @@ version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys 4.1.0",
|
||||
@@ -7066,6 +7086,15 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvim-rs"
|
||||
version = "0.6.0-pre"
|
||||
@@ -7195,6 +7224,17 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "open_ai"
|
||||
version = "0.1.0"
|
||||
@@ -7215,7 +7255,7 @@ version = "0.10.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
@@ -8042,6 +8082,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2 0.10.7",
|
||||
"shellexpand 2.1.2",
|
||||
"shlex",
|
||||
"similar",
|
||||
"smol",
|
||||
@@ -8068,6 +8109,7 @@ dependencies = [
|
||||
"db",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -8183,6 +8225,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"serde",
|
||||
@@ -8229,7 +8272,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
@@ -8278,7 +8321,9 @@ dependencies = [
|
||||
"search",
|
||||
"settings",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8485,7 +8530,6 @@ dependencies = [
|
||||
"client",
|
||||
"dev_server_projects",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -8497,6 +8541,7 @@ dependencies = [
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"task",
|
||||
"terminal_view",
|
||||
@@ -8663,6 +8708,7 @@ dependencies = [
|
||||
"image",
|
||||
"language",
|
||||
"log",
|
||||
"multi_buffer",
|
||||
"project",
|
||||
"runtimelib",
|
||||
"schemars",
|
||||
@@ -8857,6 +8903,12 @@ version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rpc"
|
||||
version = "0.1.0"
|
||||
@@ -9041,7 +9093,7 @@ version = "0.38.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"errno 0.3.8",
|
||||
"itoa",
|
||||
"libc",
|
||||
@@ -9116,7 +9168,7 @@ version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"bytemuck",
|
||||
"libm",
|
||||
"smallvec",
|
||||
@@ -9339,7 +9391,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"any_vec",
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
@@ -9767,13 +9819,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.9.0"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bc0ffd69814a9b251d43afcabf96dad1b29f5028378056257be9e3fecc9f720"
|
||||
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"log",
|
||||
"termcolor",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9926,12 +9978,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.2.0+1.5.4"
|
||||
version = "0.3.0+sdk-1.3.268.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830"
|
||||
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"num-traits",
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10107,7 +10158,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.7",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"byteorder",
|
||||
"bytes 1.5.0",
|
||||
"chrono",
|
||||
@@ -10154,7 +10205,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.7",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -10316,12 +10367,6 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.25.0"
|
||||
@@ -10576,7 +10621,7 @@ version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aef1f9d4c1dbdd1cb3a63be9efd2f04d8ddbc919d46112982c76818ffc2f1a7"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cap-fs-ext",
|
||||
"cap-std",
|
||||
"fd-lock",
|
||||
@@ -10717,9 +10762,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
@@ -10736,6 +10781,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"libc",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -10879,18 +10925,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.60"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
|
||||
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.60"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
|
||||
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -10941,7 +10987,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
@@ -11300,7 +11348,7 @@ version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"bytes 1.5.0",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -11853,7 +11901,7 @@ dependencies = [
|
||||
"kurbo",
|
||||
"log",
|
||||
"pico-args",
|
||||
"roxmltree",
|
||||
"roxmltree 0.19.0",
|
||||
"simplecss",
|
||||
"siphasher 1.0.1",
|
||||
"strict-num",
|
||||
@@ -12071,7 +12119,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cursor-icon",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -12223,7 +12271,7 @@ version = "0.201.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"indexmap 2.2.6",
|
||||
"semver",
|
||||
]
|
||||
@@ -12490,7 +12538,7 @@ checksum = "371d828b6849ea06d598ae7dd1c316e8dd9e99b76f77d93d5886cb25c7f8e188"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"bytes 1.5.0",
|
||||
"cap-fs-ext",
|
||||
"cap-net-ext",
|
||||
@@ -12577,7 +12625,7 @@ version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"rustix 0.38.32",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
@@ -12600,7 +12648,7 @@ version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
@@ -12612,7 +12660,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
@@ -12731,7 +12779,7 @@ checksum = "ae1136a209614ace00b0c11f04dc7cf42540773be3b22eff6ad165110aba29c1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"wasmtime",
|
||||
@@ -13161,7 +13209,7 @@ version = "0.36.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -13180,7 +13228,7 @@ version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"wit-bindgen-rt",
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
@@ -13236,7 +13284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"indexmap 2.2.6",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -13442,18 +13490,17 @@ name = "xim-parser"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkbcommon"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e"
|
||||
source = "git+https://github.com/ConradIrwin/xkbcommon-rs?rev=fcbb4612185cc129ceeff51d22f7fb51810a03b2#fcbb4612185cc129ceeff51d22f7fb51810a03b2"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"libc",
|
||||
"memmap2 0.8.0",
|
||||
"memmap2",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
@@ -13586,7 +13633,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.145.0"
|
||||
version = "0.146.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -13714,7 +13761,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_dart"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13728,7 +13775,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13828,9 +13875,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.0.6"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13849,7 +13896,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_ruby"
|
||||
version = "0.0.6"
|
||||
version = "0.0.8"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13864,7 +13911,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_svelte"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13899,16 +13946,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_vue"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.7",
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
13
Cargo.toml
13
Cargo.toml
@@ -274,7 +274,7 @@ zed_actions = { path = "crates/zed_actions" }
|
||||
alacritty_terminal = "0.23"
|
||||
any_vec = "0.13"
|
||||
anyhow = "1.0.57"
|
||||
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
|
||||
ashpd = "0.9.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = { version = "0.1" }
|
||||
async-fs = "1.6"
|
||||
@@ -284,10 +284,10 @@ async-trait = "0.1"
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.13"
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
blade-util = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
cap-std = "3.0"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
@@ -365,6 +365,7 @@ shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
signal-hook = "0.3.17"
|
||||
similar = "1.3"
|
||||
simplelog = "0.12.2"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
@@ -451,11 +452,11 @@ features = [
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Time",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_HiDpi",
|
||||
|
||||
3
assets/icons/chevron_down_small.svg
Normal file
3
assets/icons/chevron_down_small.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.49574 4.74787L5.99574 7.25214L8.49574 4.74787" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 246 B |
@@ -1,14 +1,13 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_32_58)">
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_62_95)">
|
||||
<path d="M4.5 6C3.67157 6 3 5.32843 3 4.5C3 3.67157 3.67157 3 4.5 3C5.32843 3 6 3.67157 6 4.5C6 5.32843 5.32843 6 4.5 6Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.54433 13.4334C6.87775 13.5227 7.22046 13.3249 7.3098 12.9914C7.39914 12.658 7.20127 12.3153 6.86786 12.226L6.54433 13.4334ZM3.77426 6.86772L3.93603 6.26401L2.72862 5.94049L2.56686 6.54419L3.77426 6.86772ZM6.86786 12.226C4.53394 11.6006 3.14889 9.20163 3.77426 6.86772L2.56686 6.54419C1.76281 9.54494 3.54359 12.6293 6.54433 13.4334L6.86786 12.226Z" fill="white"/>
|
||||
<path d="M11.5 13C10.6716 13 10 12.3284 10 11.5C10 10.6716 10.6716 10 11.5 10C12.3284 10 13 10.6716 13 11.5C13 12.3284 12.3284 13 11.5 13Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.1875 4.21113C9.88852 4.03854 9.7861 3.65629 9.95869 3.35736C10.1313 3.05843 10.5135 2.95601 10.8125 3.12859L10.1875 4.21113ZM11.7888 10.1875C12.9969 8.09496 12.28 5.41925 10.1875 4.21113L10.8125 3.12859C13.5028 4.6819 14.4246 8.12209 12.8713 10.8125L11.7888 10.1875Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_32_58">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
<clipPath id="clip0_62_95">
|
||||
<rect width="16" height="16" fill="white" transform="matrix(-1 0 0 1 16 0)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -19,7 +19,6 @@
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"shift-enter": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
@@ -79,10 +78,8 @@
|
||||
"shift-down": "editor::SelectDown",
|
||||
"shift-left": "editor::SelectLeft",
|
||||
"shift-right": "editor::SelectRight",
|
||||
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
@@ -100,6 +97,7 @@
|
||||
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
|
||||
"ctrl-'": "editor::ToggleHunkDiff",
|
||||
"ctrl-\"": "editor::ExpandAllHunkDiffs",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame"
|
||||
}
|
||||
},
|
||||
@@ -207,7 +205,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && in_replace",
|
||||
"context": "ProjectSearchBar && in_replace > Editor",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"ctrl-alt-enter": "search::ReplaceAll"
|
||||
@@ -260,20 +258,19 @@
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"shift-alt-up": "editor::AddSelectionAbove",
|
||||
"shift-alt-down": "editor::AddSelectionBelow",
|
||||
"shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
|
||||
"shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
|
||||
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding
|
||||
"ctrl-shift-down": "editor::SelectSmallerSyntaxNode", //todo(linux) tmp keybinding
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"ctrl-shift-l": "editor::SelectAllMatches",
|
||||
"ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
@@ -326,7 +323,7 @@
|
||||
"alt-9": ["pane::ActivateItem", 8],
|
||||
"alt-0": "pane::ActivateLastItem",
|
||||
"ctrl-alt--": "pane::GoBack",
|
||||
"ctrl-alt-shift--": "pane::GoForward",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
@@ -397,7 +394,7 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
@@ -566,6 +563,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
"bindings": {
|
||||
"tab": "picker::ConfirmCompletion",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal > Picker > Editor",
|
||||
"bindings": {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-l": "editor::NextScreen",
|
||||
"ctrl-l": "editor::ScrollCursorCenter",
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
@@ -102,9 +102,9 @@
|
||||
"ctrl-shift-b": "editor::SelectLeft",
|
||||
"shift-right": "editor::SelectRight",
|
||||
"ctrl-shift-f": "editor::SelectRight",
|
||||
"alt-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect
|
||||
"alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd",
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
|
||||
"alt-shift-f": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
@@ -126,7 +126,8 @@
|
||||
"cmd-alt-z": "editor::RevertSelectedHunks",
|
||||
"cmd-'": "editor::ToggleHunkDiff",
|
||||
"cmd-\"": "editor::ExpandAllHunkDiffs",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame"
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
"cmd-i": "editor::ShowSignatureHelp"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -254,7 +255,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && in_replace",
|
||||
"context": "ProjectSearchBar && in_replace > Editor",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"cmd-enter": "search::ReplaceAll"
|
||||
@@ -301,19 +302,20 @@
|
||||
"bindings": {
|
||||
"cmd-[": "editor::Outdent",
|
||||
"cmd-]": "editor::Indent",
|
||||
"cmd-alt-up": "editor::AddSelectionAbove",
|
||||
"cmd-alt-up": "editor::AddSelectionAbove", // Insert cursor above
|
||||
"cmd-ctrl-p": "editor::AddSelectionAbove",
|
||||
"cmd-alt-down": "editor::AddSelectionBelow",
|
||||
"cmd-alt-down": "editor::AddSelectionBelow", // Insert cursor below
|
||||
"cmd-ctrl-n": "editor::AddSelectionBelow",
|
||||
"cmd-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"alt-shift-up": "editor::DuplicateLineUp",
|
||||
"alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode",
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"cmd-shift-l": "editor::SelectAllMatches",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
|
||||
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
@@ -593,6 +595,14 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
"bindings": {
|
||||
"tab": "picker::ConfirmCompletion",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal > Picker > Editor",
|
||||
"bindings": {
|
||||
@@ -612,14 +622,6 @@
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker",
|
||||
"bindings": {
|
||||
"f2": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
|
||||
21
assets/keymaps/initial.json
Normal file
21
assets/keymaps/initial.json
Normal file
@@ -0,0 +1,21 @@
|
||||
// Zed keymap
|
||||
//
|
||||
// For information on binding keys, see the Zed
|
||||
// documentation: https://zed.dev/docs/key-bindings
|
||||
//
|
||||
// To see the default key bindings run `zed: Open Default Keymap`
|
||||
// from the command palette.
|
||||
[
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
// "shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
// "j k": ["workspace::SendKeystrokes", "escape"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -25,6 +25,7 @@
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
|
||||
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
|
||||
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-d": "editor::DuplicateLineDown",
|
||||
"ctrl-y": "editor::DeleteLine",
|
||||
"ctrl-m": "editor::ScrollCursorCenter",
|
||||
"ctrl-pagedown": "editor::MovePageDown",
|
||||
"ctrl-pageup": "editor::MovePageUp",
|
||||
// "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem"
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePrevItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -24,6 +24,8 @@
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"bindings": {
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem"
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePrevItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -27,6 +27,7 @@
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
[
|
||||
{
|
||||
"context": "ProjectPanel || Editor",
|
||||
"bindings": {
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimControl && !VimWaiting && !menu",
|
||||
"context": "VimControl && !menu",
|
||||
"bindings": {
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
@@ -16,8 +10,11 @@
|
||||
"backspace": "vim::Backspace",
|
||||
"j": "vim::Down",
|
||||
"down": "vim::Down",
|
||||
"ctrl-j": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"ctrl-m": "vim::NextLineStart",
|
||||
"+": "vim::NextLineStart",
|
||||
"-": "vim::PreviousLineStart",
|
||||
"tab": "vim::Tab",
|
||||
"shift-tab": "vim::Tab",
|
||||
"k": "vim::Up",
|
||||
@@ -198,20 +195,20 @@
|
||||
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||
"-": "pane::RevealInProjectPanel"
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
// escape is in its own section so that it cancels a pending count.
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"context": "VimControl && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"ctrl-[": "editor::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Cancel",
|
||||
".": "vim::Repeat",
|
||||
"c": ["vim::PushOperator", "Change"],
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
@@ -219,7 +216,7 @@
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"y": ["vim::PushOperator", "Yank"],
|
||||
"shift-y": "vim::YankLine",
|
||||
"shift-y": "vim::YankToEndOfLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
@@ -255,127 +252,12 @@
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPrevDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
"[ c": "editor::GoToPrevHunk"
|
||||
"[ c": "editor::GoToPrevHunk",
|
||||
"g c c": "vim::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"\"": ["vim::PushOperator", "Register"],
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimCount && vim_mode != insert",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == c",
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine",
|
||||
"d": "editor::Rename" // zed specific
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == c",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == gu",
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == gU",
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == g~",
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == d",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == y",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == ys",
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == >",
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == <",
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
|
||||
"t": "vim::Tag",
|
||||
"s": "vim::Sentence",
|
||||
"p": "vim::Paragraph",
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"{": "vim::CurlyBrackets",
|
||||
"}": "vim::CurlyBrackets",
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual && !VimWaiting && !VimObject",
|
||||
"context": "vim_mode == visual",
|
||||
"bindings": {
|
||||
"u": "vim::ConvertToLowerCase",
|
||||
"U": "vim::ConvertToUpperCase",
|
||||
@@ -410,23 +292,16 @@
|
||||
">": "vim::Indent",
|
||||
"<": "vim::Outdent",
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }]
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
"g c": "vim::ToggleComments",
|
||||
"\"": ["vim::PushOperator", "Register"],
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && !VimWaiting",
|
||||
"bindings": {
|
||||
"g c c": "vim::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual",
|
||||
"bindings": {
|
||||
"g c": "vim::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"context": "vim_mode == insert",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
@@ -445,30 +320,118 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == replace",
|
||||
"context": "vim_mode == replace",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"backspace": "vim::UndoReplace",
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"backspace": "vim::UndoReplace"
|
||||
"enter": "vim::Enter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode != replace && VimWaiting",
|
||||
"context": "vim_mode == waiting",
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"]
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert && VimWaiting",
|
||||
"context": "vim_mode == operator",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore"
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
|
||||
"t": "vim::Tag",
|
||||
"s": "vim::Sentence",
|
||||
"p": "vim::Paragraph",
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"{": "vim::CurlyBrackets",
|
||||
"}": "vim::CurlyBrackets",
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == c",
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine",
|
||||
"d": "editor::Rename", // zed specific
|
||||
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gu",
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gU",
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == g~",
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == ys",
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == >",
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == <",
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -508,7 +471,8 @@
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent"
|
||||
"-": "project_panel::SelectParent",
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
241
assets/prompts/operations.md
Normal file
241
assets/prompts/operations.md
Normal file
@@ -0,0 +1,241 @@
|
||||
Your task is to map a step from the conversation above to operations on symbols inside the provided source files.
|
||||
|
||||
Guidelines:
|
||||
- There's no need to describe *what* to do, just *where* to do it.
|
||||
- If creating a file, assume any subsequent updates are included at the time of creation.
|
||||
- Don't create and then update a file.
|
||||
- We'll create it in one shot.
|
||||
- Prefer updating symbols lower in the syntax tree if possible.
|
||||
- Never include operations on a parent symbol and one of its children in the same <operations> block.
|
||||
- Never nest an operation with another operation or include CDATA or other content. All operations are leaf nodes.
|
||||
- Include a description attribute for each operation with a brief, one-line description of the change to perform.
|
||||
- Descriptions are required for all operations except delete.
|
||||
- When generating multiple operations, ensure the descriptions are specific to each individual operation.
|
||||
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
|
||||
- Don't generate multiple operations at the same location. Instead, combine them together in a single operation with a succinct combined description.
|
||||
|
||||
The available operation types are:
|
||||
|
||||
1. <update>: Modify an existing symbol in a file.
|
||||
2. <create_file>: Create a new file.
|
||||
3. <insert_sibling_after>: Add a new symbol as sibling after an existing symbol in a file.
|
||||
4. <append_child>: Add a new symbol as the last child of an existing symbol in a file.
|
||||
5. <prepend_child>: Add a new symbol as the first child of an existing symbol in a file.
|
||||
6. <delete>: Remove an existing symbol from a file. The `description` attribute is invalid for delete, but required for other ops.
|
||||
|
||||
All operations *require* a path.
|
||||
Operations that *require* a symbol: <update>, <insert_sibling_after>, <delete>
|
||||
Operations that don't allow a symbol: <create>
|
||||
Operations that have an *optional* symbol: <prepend_child>, <append_child>
|
||||
|
||||
Example 1:
|
||||
|
||||
User:
|
||||
```rs src/rectangle.rs
|
||||
struct Rectangle {
|
||||
width: f64,
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Symbols for src/rectangle.rs:
|
||||
- struct Rectangle
|
||||
- impl Rectangle
|
||||
- impl Rectangle fn new
|
||||
|
||||
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
|
||||
<step>Implement the 'Display' trait for the Rectangle struct</step>
|
||||
|
||||
What are the operations for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
|
||||
|
||||
Assistant (wrong):
|
||||
<operations>
|
||||
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_area method" />
|
||||
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_perimeter method" />
|
||||
</operations>
|
||||
|
||||
This demonstrates what NOT to do. NEVER append multiple children at the same location.
|
||||
|
||||
Assistant (corrected):
|
||||
<operations>
|
||||
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate area and perimeter methods" />
|
||||
</operations>
|
||||
|
||||
User:
|
||||
What are the operations for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<insert_sibling_after path="src/shapes.rs" symbol="impl Rectangle" description="Implement Display trait for Rectangle"/>
|
||||
</operations>
|
||||
|
||||
Example 2:
|
||||
|
||||
User:
|
||||
```rs src/user.rs
|
||||
struct User {
|
||||
pub name: String,
|
||||
age: u32,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
}
|
||||
|
||||
pub fn print_info(&self) {
|
||||
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Symbols for src/user.rs:
|
||||
- struct User
|
||||
- struct User pub name
|
||||
- struct User age
|
||||
- struct User email
|
||||
- impl User
|
||||
- impl User fn new
|
||||
- impl User pub fn print_info
|
||||
|
||||
<step>Update the 'print_info' method to use formatted output</step>
|
||||
<step>Remove the 'email' field from the User struct</step>
|
||||
|
||||
What are the operations for the step: <step>Update the 'print_info' method to use formatted output</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<update path="src/user.rs" symbol="impl User fn print_info" description="Use formatted output" />
|
||||
</operations>
|
||||
|
||||
User:
|
||||
What are the operations for the step: <step>Remove the 'email' field from the User struct</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<delete path="src/user.rs" symbol="struct User email" description="Remove the email field" />
|
||||
</operations>
|
||||
|
||||
Example 3:
|
||||
|
||||
User:
|
||||
```rs src/vehicle.rs
|
||||
struct Vehicle {
|
||||
make: String,
|
||||
model: String,
|
||||
year: u32,
|
||||
}
|
||||
|
||||
impl Vehicle {
|
||||
fn new(make: String, model: String, year: u32) -> Self {
|
||||
Vehicle { make, model, year }
|
||||
}
|
||||
|
||||
fn print_year(&self) {
|
||||
println!("Year: {}", self.year);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Symbols for src/vehicle.rs:
|
||||
- struct Vehicle
|
||||
- struct Vehicle make
|
||||
- struct Vehicle model
|
||||
- struct Vehicle year
|
||||
- impl Vehicle
|
||||
- impl Vehicle fn new
|
||||
- impl Vehicle fn print_year
|
||||
|
||||
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
|
||||
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
|
||||
|
||||
What are the operations for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<prepend_child path="src/vehicle.rs" description="Add 'use std::fmt' statement" />
|
||||
</operations>
|
||||
|
||||
User:
|
||||
What are the operations for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<insert_sibling_after path="src/vehicle.rs" symbol="impl Vehicle fn new" description="Add start_engine method"/>
|
||||
</operations>
|
||||
|
||||
Example 4:
|
||||
|
||||
User:
|
||||
```rs src/employee.rs
|
||||
struct Employee {
|
||||
name: String,
|
||||
position: String,
|
||||
salary: u32,
|
||||
department: String,
|
||||
}
|
||||
|
||||
impl Employee {
|
||||
fn new(name: String, position: String, salary: u32, department: String) -> Self {
|
||||
Employee { name, position, salary, department }
|
||||
}
|
||||
|
||||
fn print_details(&self) {
|
||||
println!("Name: {}, Position: {}, Salary: {}, Department: {}",
|
||||
self.name, self.position, self.salary, self.department);
|
||||
}
|
||||
|
||||
fn give_raise(&mut self, amount: u32) {
|
||||
self.salary += amount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Symbols for src/employee.rs:
|
||||
- struct Employee
|
||||
- struct Employee name
|
||||
- struct Employee position
|
||||
- struct Employee salary
|
||||
- struct Employee department
|
||||
- impl Employee
|
||||
- impl Employee fn new
|
||||
- impl Employee fn print_details
|
||||
- impl Employee fn give_raise
|
||||
|
||||
<step>Make salary an f32</step>
|
||||
|
||||
What are the operations for the step: <step>Make salary an f32</step>
|
||||
|
||||
A (wrong):
|
||||
<operations>
|
||||
<update path="src/employee.rs" symbol="struct Employee" description="Change the type of salary to an f32" />
|
||||
<update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
|
||||
</operations>
|
||||
|
||||
This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`.
|
||||
|
||||
A (corrected):
|
||||
<operations>
|
||||
<update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
|
||||
</operations>
|
||||
|
||||
User:
|
||||
What are the correct operations for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
|
||||
|
||||
A:
|
||||
<operations>
|
||||
<delete path="src/employee.rs" symbol="struct Employee department" />
|
||||
<update path="src/employee.rs" symbol="impl Employee fn print_details" description="Don't print the 'department' field" />
|
||||
</operations>
|
||||
|
||||
Now generate the operations for the following step.
|
||||
Output only valid XML containing valid operations with their required attributes.
|
||||
NEVER output code or any other text inside <operation> tags. If you do, you will replaced with another model.
|
||||
Your response *MUST* begin with <operations> and end with </operations>:
|
||||
@@ -47,7 +47,7 @@
|
||||
// },
|
||||
"buffer_line_height": "comfortable",
|
||||
// The name of a font to use for rendering text in the UI
|
||||
// (On macOS) You can set this to ".SysmtemUIFont" to use the system font
|
||||
// (On macOS) You can set this to ".SystemUIFont" to use the system font
|
||||
"ui_font_family": "Zed Plex Sans",
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
@@ -94,6 +94,9 @@
|
||||
// 3. Never close the window
|
||||
// "when_closing_with_no_tabs": "keep_window_open",
|
||||
"when_closing_with_no_tabs": "platform_default",
|
||||
// Whether to use the system provided dialogs for Open and Save As.
|
||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||
"use_system_path_prompts": true,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// How to highlight the current line in the editor.
|
||||
@@ -116,6 +119,11 @@
|
||||
// The debounce delay before re-querying the language server for completion
|
||||
// documentation when not included in original completion list.
|
||||
"completion_documentation_secondary_query_debounce": 300,
|
||||
// Show method signatures in the editor, when inside parentheses.
|
||||
"auto_signature_help": false,
|
||||
/// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": true,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if softwrap is set to 'preferred_line_length', and will show any
|
||||
@@ -128,14 +136,7 @@
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 3,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
@@ -262,6 +263,8 @@
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
"relative_line_numbers": false,
|
||||
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
||||
"search_wrap": true,
|
||||
// When to populate a new search's query based on the text under the cursor.
|
||||
// This setting can take the following three values:
|
||||
//
|
||||
@@ -393,6 +396,7 @@
|
||||
// 2. "gpt-4"
|
||||
// 3. "gpt-4-turbo-preview"
|
||||
// 4. "gpt-4o"
|
||||
// 5. "gpt-4o-mini"
|
||||
"default_model": "gpt-4o"
|
||||
}
|
||||
},
|
||||
@@ -431,7 +435,9 @@
|
||||
// Show git status colors in the editor tabs.
|
||||
"git_status": false,
|
||||
// Position of the close button on the editor tabs.
|
||||
"close_position": "right"
|
||||
"close_position": "right",
|
||||
// Whether to show the file icon for a tab.
|
||||
"file_icons": false
|
||||
},
|
||||
// Settings related to preview tabs.
|
||||
"preview_tabs": {
|
||||
@@ -539,6 +545,14 @@
|
||||
// "delay_ms": 600
|
||||
}
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration through the shell hook, works for POSIX shells and fish.
|
||||
// "load_direnv": "shell_hook"
|
||||
// 2. Load direnv configuration using `direnv export json` directly.
|
||||
// This can help with some shells that otherwise would not detect
|
||||
// the direnv environment, such as nushell or elvish.
|
||||
// "load_direnv": "direct"
|
||||
"load_direnv": "shell_hook",
|
||||
"inline_completions": {
|
||||
// A list of globs representing files that inline completions should be disabled for.
|
||||
"disabled_globs": [".env"]
|
||||
@@ -714,10 +728,12 @@
|
||||
}
|
||||
},
|
||||
"C": {
|
||||
"format_on_save": "off"
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false
|
||||
},
|
||||
"C++": {
|
||||
"format_on_save": "off"
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false
|
||||
},
|
||||
"CSS": {
|
||||
"prettier": {
|
||||
@@ -769,11 +785,13 @@
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false,
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"PHP": {
|
||||
"language_servers": ["phpactor", "!intelephense", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-php"],
|
||||
@@ -781,7 +799,7 @@
|
||||
}
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "..."]
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
|
||||
},
|
||||
"SCSS": {
|
||||
"prettier": {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// documentation: https://zed.dev/docs/configuring-zed
|
||||
//
|
||||
// To see all of Zed's default settings without changing your
|
||||
// custom settings, run the `open default settings` command
|
||||
// from the command palette or from `Zed` application menu.
|
||||
// custom settings, run the `zed: Open Default Settings` command
|
||||
// from the command palette
|
||||
{
|
||||
"ui_font_size": 16,
|
||||
"buffer_font_size": 16,
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#10a793ff",
|
||||
"status_bar.background": "#262933ff",
|
||||
"title_bar.background": "#262933ff",
|
||||
"title_bar.inactive_background": "#21242bff",
|
||||
"toolbar.background": "#1e2025ff",
|
||||
"tab_bar.background": "#21242bff",
|
||||
"tab.inactive_background": "#21242bff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#566ddaff",
|
||||
"status_bar.background": "#3a353fff",
|
||||
"title_bar.background": "#3a353fff",
|
||||
"title_bar.inactive_background": "#221f26ff",
|
||||
"toolbar.background": "#19171cff",
|
||||
"tab_bar.background": "#221f26ff",
|
||||
"tab.inactive_background": "#221f26ff",
|
||||
@@ -422,6 +423,7 @@
|
||||
"icon.accent": "#586cdaff",
|
||||
"status_bar.background": "#bfbcc5ff",
|
||||
"title_bar.background": "#bfbcc5ff",
|
||||
"title_bar.inactive_background": "#e6e3ebff",
|
||||
"toolbar.background": "#efecf4ff",
|
||||
"tab_bar.background": "#e6e3ebff",
|
||||
"tab.inactive_background": "#e6e3ebff",
|
||||
@@ -806,6 +808,7 @@
|
||||
"icon.accent": "#6684e0ff",
|
||||
"status_bar.background": "#45433bff",
|
||||
"title_bar.background": "#45433bff",
|
||||
"title_bar.inactive_background": "#262622ff",
|
||||
"toolbar.background": "#20201dff",
|
||||
"tab_bar.background": "#262622ff",
|
||||
"tab.inactive_background": "#262622ff",
|
||||
@@ -1190,6 +1193,7 @@
|
||||
"icon.accent": "#6684dfff",
|
||||
"status_bar.background": "#cecab4ff",
|
||||
"title_bar.background": "#cecab4ff",
|
||||
"title_bar.inactive_background": "#eeebd7ff",
|
||||
"toolbar.background": "#fefbecff",
|
||||
"tab_bar.background": "#eeebd7ff",
|
||||
"tab.inactive_background": "#eeebd7ff",
|
||||
@@ -1574,6 +1578,7 @@
|
||||
"icon.accent": "#36a165ff",
|
||||
"status_bar.background": "#424136ff",
|
||||
"title_bar.background": "#424136ff",
|
||||
"title_bar.inactive_background": "#2c2b23ff",
|
||||
"toolbar.background": "#22221bff",
|
||||
"tab_bar.background": "#2c2b23ff",
|
||||
"tab.inactive_background": "#2c2b23ff",
|
||||
@@ -1958,6 +1963,7 @@
|
||||
"icon.accent": "#37a165ff",
|
||||
"status_bar.background": "#c5c4b9ff",
|
||||
"title_bar.background": "#c5c4b9ff",
|
||||
"title_bar.inactive_background": "#ebeae3ff",
|
||||
"toolbar.background": "#f4f3ecff",
|
||||
"tab_bar.background": "#ebeae3ff",
|
||||
"tab.inactive_background": "#ebeae3ff",
|
||||
@@ -2342,6 +2348,7 @@
|
||||
"icon.accent": "#407ee6ff",
|
||||
"status_bar.background": "#443c39ff",
|
||||
"title_bar.background": "#443c39ff",
|
||||
"title_bar.inactive_background": "#27211eff",
|
||||
"toolbar.background": "#1b1918ff",
|
||||
"tab_bar.background": "#27211eff",
|
||||
"tab.inactive_background": "#27211eff",
|
||||
@@ -2726,6 +2733,7 @@
|
||||
"icon.accent": "#407ee6ff",
|
||||
"status_bar.background": "#ccc7c5ff",
|
||||
"title_bar.background": "#ccc7c5ff",
|
||||
"title_bar.inactive_background": "#e9e6e4ff",
|
||||
"toolbar.background": "#f0eeedff",
|
||||
"tab_bar.background": "#e9e6e4ff",
|
||||
"tab.inactive_background": "#e9e6e4ff",
|
||||
@@ -3110,6 +3118,7 @@
|
||||
"icon.accent": "#5169ebff",
|
||||
"status_bar.background": "#433a43ff",
|
||||
"title_bar.background": "#433a43ff",
|
||||
"title_bar.inactive_background": "#252025ff",
|
||||
"toolbar.background": "#1b181bff",
|
||||
"tab_bar.background": "#252025ff",
|
||||
"tab.inactive_background": "#252025ff",
|
||||
@@ -3494,6 +3503,7 @@
|
||||
"icon.accent": "#5169ebff",
|
||||
"status_bar.background": "#c6b8c6ff",
|
||||
"title_bar.background": "#c6b8c6ff",
|
||||
"title_bar.inactive_background": "#e0d5e0ff",
|
||||
"toolbar.background": "#f7f3f7ff",
|
||||
"tab_bar.background": "#e0d5e0ff",
|
||||
"tab.inactive_background": "#e0d5e0ff",
|
||||
@@ -3878,6 +3888,7 @@
|
||||
"icon.accent": "#267eadff",
|
||||
"status_bar.background": "#33444dff",
|
||||
"title_bar.background": "#33444dff",
|
||||
"title_bar.inactive_background": "#1c2529ff",
|
||||
"toolbar.background": "#161b1dff",
|
||||
"tab_bar.background": "#1c2529ff",
|
||||
"tab.inactive_background": "#1c2529ff",
|
||||
@@ -4262,6 +4273,7 @@
|
||||
"icon.accent": "#267eadff",
|
||||
"status_bar.background": "#a6cadcff",
|
||||
"title_bar.background": "#a6cadcff",
|
||||
"title_bar.inactive_background": "#cdeaf9ff",
|
||||
"toolbar.background": "#ebf8ffff",
|
||||
"tab_bar.background": "#cdeaf9ff",
|
||||
"tab.inactive_background": "#cdeaf9ff",
|
||||
@@ -4646,6 +4658,7 @@
|
||||
"icon.accent": "#7272caff",
|
||||
"status_bar.background": "#3b3535ff",
|
||||
"title_bar.background": "#3b3535ff",
|
||||
"title_bar.inactive_background": "#252020ff",
|
||||
"toolbar.background": "#1b1818ff",
|
||||
"tab_bar.background": "#252020ff",
|
||||
"tab.inactive_background": "#252020ff",
|
||||
@@ -5030,6 +5043,7 @@
|
||||
"icon.accent": "#7272caff",
|
||||
"status_bar.background": "#c1bbbbff",
|
||||
"title_bar.background": "#c1bbbbff",
|
||||
"title_bar.inactive_background": "#ebe3e3ff",
|
||||
"toolbar.background": "#f4ececff",
|
||||
"tab_bar.background": "#ebe3e3ff",
|
||||
"tab.inactive_background": "#ebe3e3ff",
|
||||
@@ -5414,6 +5428,7 @@
|
||||
"icon.accent": "#468b8fff",
|
||||
"status_bar.background": "#353f39ff",
|
||||
"title_bar.background": "#353f39ff",
|
||||
"title_bar.inactive_background": "#1f2621ff",
|
||||
"toolbar.background": "#171c19ff",
|
||||
"tab_bar.background": "#1f2621ff",
|
||||
"tab.inactive_background": "#1f2621ff",
|
||||
@@ -5798,6 +5813,7 @@
|
||||
"icon.accent": "#488b90ff",
|
||||
"status_bar.background": "#bcc5bfff",
|
||||
"title_bar.background": "#bcc5bfff",
|
||||
"title_bar.inactive_background": "#e3ebe6ff",
|
||||
"toolbar.background": "#ecf4eeff",
|
||||
"tab_bar.background": "#e3ebe6ff",
|
||||
"tab.inactive_background": "#e3ebe6ff",
|
||||
@@ -6182,6 +6198,7 @@
|
||||
"icon.accent": "#3e62f4ff",
|
||||
"status_bar.background": "#3b453bff",
|
||||
"title_bar.background": "#3b453bff",
|
||||
"title_bar.inactive_background": "#1f231fff",
|
||||
"toolbar.background": "#131513ff",
|
||||
"tab_bar.background": "#1f231fff",
|
||||
"tab.inactive_background": "#1f231fff",
|
||||
@@ -6566,6 +6583,7 @@
|
||||
"icon.accent": "#3e61f4ff",
|
||||
"status_bar.background": "#b4ceb4ff",
|
||||
"title_bar.background": "#b4ceb4ff",
|
||||
"title_bar.inactive_background": "#daeedaff",
|
||||
"toolbar.background": "#f3faf3ff",
|
||||
"tab_bar.background": "#daeedaff",
|
||||
"tab.inactive_background": "#daeedaff",
|
||||
@@ -6950,6 +6968,7 @@
|
||||
"icon.accent": "#3e8ed0ff",
|
||||
"status_bar.background": "#3e4769ff",
|
||||
"title_bar.background": "#3e4769ff",
|
||||
"title_bar.inactive_background": "#262f51ff",
|
||||
"toolbar.background": "#202646ff",
|
||||
"tab_bar.background": "#262f51ff",
|
||||
"tab.inactive_background": "#262f51ff",
|
||||
@@ -7334,6 +7353,7 @@
|
||||
"icon.accent": "#3e8fd0ff",
|
||||
"status_bar.background": "#c1c5d8ff",
|
||||
"title_bar.background": "#c1c5d8ff",
|
||||
"title_bar.inactive_background": "#e5e8f5ff",
|
||||
"toolbar.background": "#f5f7ffff",
|
||||
"tab_bar.background": "#e5e8f5ff",
|
||||
"tab.inactive_background": "#e5e8f5ff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#5ac1feff",
|
||||
"status_bar.background": "#313337ff",
|
||||
"title_bar.background": "#313337ff",
|
||||
"title_bar.inactive_background": "#1f2127ff",
|
||||
"toolbar.background": "#0d1016ff",
|
||||
"tab_bar.background": "#1f2127ff",
|
||||
"tab.inactive_background": "#1f2127ff",
|
||||
@@ -407,6 +408,7 @@
|
||||
"icon.accent": "#3b9ee5ff",
|
||||
"status_bar.background": "#dcdddeff",
|
||||
"title_bar.background": "#dcdddeff",
|
||||
"title_bar.inactive_background": "#ececedff",
|
||||
"toolbar.background": "#fcfcfcff",
|
||||
"tab_bar.background": "#ececedff",
|
||||
"tab.inactive_background": "#ececedff",
|
||||
@@ -776,6 +778,7 @@
|
||||
"icon.accent": "#72cffeff",
|
||||
"status_bar.background": "#464a52ff",
|
||||
"title_bar.background": "#464a52ff",
|
||||
"title_bar.inactive_background": "#353944ff",
|
||||
"toolbar.background": "#242835ff",
|
||||
"tab_bar.background": "#353944ff",
|
||||
"tab.inactive_background": "#353944ff",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"icon.accent": "#83a598ff",
|
||||
"status_bar.background": "#4c4642ff",
|
||||
"title_bar.background": "#4c4642ff",
|
||||
"title_bar.inactive_background": "#3a3735ff",
|
||||
"toolbar.background": "#282828ff",
|
||||
"tab_bar.background": "#3a3735ff",
|
||||
"tab.inactive_background": "#3a3735ff",
|
||||
@@ -430,6 +431,7 @@
|
||||
"icon.accent": "#83a598ff",
|
||||
"status_bar.background": "#4c4642ff",
|
||||
"title_bar.background": "#4c4642ff",
|
||||
"title_bar.inactive_background": "#393634ff",
|
||||
"toolbar.background": "#1d2021ff",
|
||||
"tab_bar.background": "#393634ff",
|
||||
"tab.inactive_background": "#393634ff",
|
||||
@@ -813,6 +815,7 @@
|
||||
"icon.accent": "#83a598ff",
|
||||
"status_bar.background": "#4c4642ff",
|
||||
"title_bar.background": "#4c4642ff",
|
||||
"title_bar.inactive_background": "#3b3735ff",
|
||||
"toolbar.background": "#32302fff",
|
||||
"tab_bar.background": "#3b3735ff",
|
||||
"tab.inactive_background": "#3b3735ff",
|
||||
@@ -1196,6 +1199,7 @@
|
||||
"icon.accent": "#0b6678ff",
|
||||
"status_bar.background": "#d9c8a4ff",
|
||||
"title_bar.background": "#d9c8a4ff",
|
||||
"title_bar.inactive_background": "#ecddb4ff",
|
||||
"toolbar.background": "#fbf1c7ff",
|
||||
"tab_bar.background": "#ecddb4ff",
|
||||
"tab.inactive_background": "#ecddb4ff",
|
||||
@@ -1579,6 +1583,7 @@
|
||||
"icon.accent": "#0b6678ff",
|
||||
"status_bar.background": "#d9c8a4ff",
|
||||
"title_bar.background": "#d9c8a4ff",
|
||||
"title_bar.inactive_background": "#ecddb5ff",
|
||||
"toolbar.background": "#f9f5d7ff",
|
||||
"tab_bar.background": "#ecddb5ff",
|
||||
"tab.inactive_background": "#ecddb5ff",
|
||||
@@ -1962,6 +1967,7 @@
|
||||
"icon.accent": "#0b6678ff",
|
||||
"status_bar.background": "#d9c8a4ff",
|
||||
"title_bar.background": "#d9c8a4ff",
|
||||
"title_bar.inactive_background": "#ecdcb3ff",
|
||||
"toolbar.background": "#f2e5bcff",
|
||||
"tab_bar.background": "#ecdcb3ff",
|
||||
"tab.inactive_background": "#ecdcb3ff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#74ade8ff",
|
||||
"status_bar.background": "#3b414dff",
|
||||
"title_bar.background": "#3b414dff",
|
||||
"title_bar.inactive_background": "#2e343eff",
|
||||
"toolbar.background": "#282c33ff",
|
||||
"tab_bar.background": "#2f343eff",
|
||||
"tab.inactive_background": "#2f343eff",
|
||||
@@ -412,6 +413,7 @@
|
||||
"icon.accent": "#5c78e2ff",
|
||||
"status_bar.background": "#dcdcddff",
|
||||
"title_bar.background": "#dcdcddff",
|
||||
"title_bar.inactive_background": "#ebebecff",
|
||||
"toolbar.background": "#fafafaff",
|
||||
"tab_bar.background": "#ebebecff",
|
||||
"tab.inactive_background": "#ebebecff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#9bced6ff",
|
||||
"status_bar.background": "#292738ff",
|
||||
"title_bar.background": "#292738ff",
|
||||
"title_bar.inactive_background": "#1c1b2aff",
|
||||
"toolbar.background": "#191724ff",
|
||||
"tab_bar.background": "#1c1b2aff",
|
||||
"tab.inactive_background": "#1c1b2aff",
|
||||
@@ -417,6 +418,7 @@
|
||||
"icon.accent": "#57949fff",
|
||||
"status_bar.background": "#dcd8d8ff",
|
||||
"title_bar.background": "#dcd8d8ff",
|
||||
"title_bar.inactive_background": "#fef9f2ff",
|
||||
"toolbar.background": "#faf4edff",
|
||||
"tab_bar.background": "#fef9f2ff",
|
||||
"tab.inactive_background": "#fef9f2ff",
|
||||
@@ -796,6 +798,7 @@
|
||||
"icon.accent": "#9bced6ff",
|
||||
"status_bar.background": "#38354eff",
|
||||
"title_bar.background": "#38354eff",
|
||||
"title_bar.inactive_background": "#28253cff",
|
||||
"toolbar.background": "#232136ff",
|
||||
"tab_bar.background": "#28253cff",
|
||||
"tab.inactive_background": "#28253cff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#518b8bff",
|
||||
"status_bar.background": "#333944ff",
|
||||
"title_bar.background": "#333944ff",
|
||||
"title_bar.inactive_background": "#2b3038ff",
|
||||
"toolbar.background": "#282c33ff",
|
||||
"tab_bar.background": "#2b3038ff",
|
||||
"tab.inactive_background": "#2b3038ff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#278ad1ff",
|
||||
"status_bar.background": "#073743ff",
|
||||
"title_bar.background": "#073743ff",
|
||||
"title_bar.inactive_background": "#04313bff",
|
||||
"toolbar.background": "#002a35ff",
|
||||
"tab_bar.background": "#04313bff",
|
||||
"tab.inactive_background": "#04313bff",
|
||||
@@ -407,6 +408,7 @@
|
||||
"icon.accent": "#288bd1ff",
|
||||
"status_bar.background": "#cfd0c4ff",
|
||||
"title_bar.background": "#cfd0c4ff",
|
||||
"title_bar.inactive_background": "#f3eddaff",
|
||||
"toolbar.background": "#fdf6e3ff",
|
||||
"tab_bar.background": "#f3eddaff",
|
||||
"tab.inactive_background": "#f3eddaff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#499befff",
|
||||
"status_bar.background": "#2a261cff",
|
||||
"title_bar.background": "#2a261cff",
|
||||
"title_bar.inactive_background": "#231f16ff",
|
||||
"toolbar.background": "#1b1810ff",
|
||||
"tab_bar.background": "#231f16ff",
|
||||
"tab.inactive_background": "#231f16ff",
|
||||
|
||||
@@ -107,6 +107,7 @@ impl ActivityIndicator {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
@@ -23,6 +23,7 @@ test-support = [
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
async-watch.workspace = true
|
||||
breadcrumbs.workspace = true
|
||||
@@ -63,7 +64,6 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
strsim = "0.11"
|
||||
strum.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
@@ -76,6 +76,7 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
picker.workspace = true
|
||||
roxmltree = "0.20.0"
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -7,7 +7,6 @@ mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
mod terminal_inline_assistant;
|
||||
@@ -21,7 +20,7 @@ pub use completion_provider::*;
|
||||
pub use context::*;
|
||||
pub use context_store::*;
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use gpui::{actions, impl_actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
pub(crate) use inline_assistant::*;
|
||||
pub(crate) use model_selector::*;
|
||||
@@ -30,8 +29,8 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, default_command, diagnostics_command, docs_command, fetch_command,
|
||||
file_command, now_command, project_command, prompt_command, search_command, tabs_command,
|
||||
term_command,
|
||||
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
|
||||
tabs_command, term_command,
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
@@ -49,16 +48,22 @@ actions!(
|
||||
InsertIntoEditor,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
InsertActivePrompt,
|
||||
DeployHistory,
|
||||
DeployPromptLibrary,
|
||||
ApplyEdit,
|
||||
ConfirmCommand,
|
||||
ToggleModelSelector
|
||||
ToggleModelSelector,
|
||||
DebugEditSteps
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct InlineAssist {
|
||||
prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl_actions!(assistant, [InlineAssist]);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct MessageId(clock::Lamport);
|
||||
|
||||
@@ -367,6 +372,7 @@ fn register_slash_commands(cx: &mut AppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
||||
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
|
||||
slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
|
||||
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
||||
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,10 +23,13 @@ pub enum CloudModel {
|
||||
Gpt4Turbo,
|
||||
#[default]
|
||||
Gpt4Omni,
|
||||
Gpt4OmniMini,
|
||||
Claude3_5Sonnet,
|
||||
Claude3Opus,
|
||||
Claude3Sonnet,
|
||||
Claude3Haiku,
|
||||
Gemini15Pro,
|
||||
Gemini15Flash,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
@@ -105,10 +108,13 @@ impl CloudModel {
|
||||
Self::Gpt4 => "gpt-4",
|
||||
Self::Gpt4Turbo => "gpt-4-turbo-preview",
|
||||
Self::Gpt4Omni => "gpt-4o",
|
||||
Self::Gpt4OmniMini => "gpt-4o-mini",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
Self::Claude3Opus => "claude-3-opus",
|
||||
Self::Claude3Sonnet => "claude-3-sonnet",
|
||||
Self::Claude3Haiku => "claude-3-haiku",
|
||||
Self::Gemini15Pro => "gemini-1.5-pro",
|
||||
Self::Gemini15Flash => "gemini-1.5-flash",
|
||||
Self::Custom(id) => id,
|
||||
}
|
||||
}
|
||||
@@ -119,10 +125,13 @@ impl CloudModel {
|
||||
Self::Gpt4 => "GPT 4",
|
||||
Self::Gpt4Turbo => "GPT 4 Turbo",
|
||||
Self::Gpt4Omni => "GPT 4 Omni",
|
||||
Self::Gpt4OmniMini => "GPT 4 Omni Mini",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
Self::Gemini15Pro => "Gemini 1.5 Pro",
|
||||
Self::Gemini15Flash => "Gemini 1.5 Flash",
|
||||
Self::Custom(id) => id.as_str(),
|
||||
}
|
||||
}
|
||||
@@ -132,10 +141,13 @@ impl CloudModel {
|
||||
Self::Gpt3Point5Turbo => 2048,
|
||||
Self::Gpt4 => 4096,
|
||||
Self::Gpt4Turbo | Self::Gpt4Omni => 128000,
|
||||
Self::Gpt4OmniMini => 128000,
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => 200000,
|
||||
Self::Gemini15Pro => 128000,
|
||||
Self::Gemini15Flash => 32000,
|
||||
Self::Custom(_) => 4096, // TODO: Make this configurable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,10 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
use futures::{future::BoxFuture, stream::BoxStream};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, StreamExt};
|
||||
use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::time::Duration;
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::{any::Any, pin::Pin, sync::Arc, task::Poll, time::Duration};
|
||||
|
||||
/// Choose which model to use for openai provider.
|
||||
/// If the model is not available, try to use the first available model, or fallback to the original model.
|
||||
@@ -55,10 +54,21 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub struct CompletionResponse {
|
||||
pub inner: BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>,
|
||||
inner: BoxStream<'static, Result<String>>,
|
||||
_lock: SemaphoreGuardArc,
|
||||
}
|
||||
|
||||
impl futures::Stream for CompletionResponse {
|
||||
type Item = Result<String>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
Pin::new(&mut self.inner).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelCompletionProvider: Send + Sync {
|
||||
fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel>;
|
||||
fn settings_version(&self) -> usize;
|
||||
@@ -72,7 +82,7 @@ pub trait LanguageModelCompletionProvider: Send + Sync {
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>>;
|
||||
fn complete(
|
||||
fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
|
||||
@@ -136,20 +146,34 @@ impl CompletionProvider {
|
||||
self.provider.read().count_tokens(request, cx)
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
pub fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> Task<CompletionResponse> {
|
||||
) -> Task<Result<CompletionResponse>> {
|
||||
let rate_limiter = self.request_limiter.clone();
|
||||
let provider = self.provider.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let lock = rate_limiter.acquire_arc().await;
|
||||
let response = provider.read().complete(request);
|
||||
CompletionResponse {
|
||||
let response = provider.read().stream_completion(request);
|
||||
let response = response.await?;
|
||||
Ok(CompletionResponse {
|
||||
inner: response,
|
||||
_lock: lock,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn complete(&self, request: LanguageModelRequest, cx: &AppContext) -> Task<Result<String>> {
|
||||
let response = self.stream_completion(request, cx);
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let mut chunks = response.await?;
|
||||
let mut completion = String::new();
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
let chunk = chunk?;
|
||||
completion.push_str(&chunk);
|
||||
}
|
||||
Ok(completion)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -300,7 +324,7 @@ mod tests {
|
||||
|
||||
// Enqueue some requests
|
||||
for i in 0..MAX_CONCURRENT_COMPLETION_REQUESTS * 2 {
|
||||
let response = provider.complete(
|
||||
let response = provider.stream_completion(
|
||||
LanguageModelRequest {
|
||||
temperature: i as f32 / 10.0,
|
||||
..Default::default()
|
||||
@@ -309,8 +333,7 @@ mod tests {
|
||||
);
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let response = response.await;
|
||||
let mut stream = response.inner.await.unwrap();
|
||||
let mut stream = response.await.unwrap();
|
||||
while let Some(message) = stream.next().await {
|
||||
message.unwrap();
|
||||
}
|
||||
@@ -326,7 +349,7 @@ mod tests {
|
||||
|
||||
// Get the first completion request that is in flight and mark it as completed.
|
||||
let completion = fake_provider
|
||||
.running_completions()
|
||||
.pending_completions()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
@@ -347,7 +370,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Mark all completion requests as finished that are in flight.
|
||||
for request in fake_provider.running_completions() {
|
||||
for request in fake_provider.pending_completions() {
|
||||
fake_provider.finish_completion(&request);
|
||||
}
|
||||
|
||||
@@ -362,7 +385,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Finish all remaining completion requests.
|
||||
for request in fake_provider.running_completions() {
|
||||
for request in fake_provider.pending_completions() {
|
||||
fake_provider.finish_completion(&request);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ impl LanguageModelCompletionProvider for AnthropicCompletionProvider {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
|
||||
fn complete(
|
||||
fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
|
||||
@@ -135,7 +135,7 @@ impl LanguageModelCompletionProvider for CloudCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn complete(
|
||||
fn stream_completion(
|
||||
&self,
|
||||
mut request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
|
||||
@@ -23,7 +23,7 @@ impl FakeCompletionProvider {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn running_completions(&self) -> Vec<LanguageModelRequest> {
|
||||
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.keys()
|
||||
@@ -35,7 +35,7 @@ impl FakeCompletionProvider {
|
||||
self.current_completion_txs.lock().len()
|
||||
}
|
||||
|
||||
pub fn send_completion(&self, request: &LanguageModelRequest, chunk: String) {
|
||||
pub fn send_completion_chunk(&self, request: &LanguageModelRequest, chunk: String) {
|
||||
let json = serde_json::to_string(request).unwrap();
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
@@ -45,10 +45,19 @@ impl FakeCompletionProvider {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn send_last_completion_chunk(&self, chunk: String) {
|
||||
self.send_completion_chunk(self.pending_completions().last().unwrap(), chunk);
|
||||
}
|
||||
|
||||
pub fn finish_completion(&self, request: &LanguageModelRequest) {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.remove(&serde_json::to_string(request).unwrap());
|
||||
.remove(&serde_json::to_string(request).unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn finish_last_completion(&self) {
|
||||
self.finish_completion(self.pending_completions().last().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +98,7 @@ impl LanguageModelCompletionProvider for FakeCompletionProvider {
|
||||
futures::future::ready(Ok(0)).boxed()
|
||||
}
|
||||
|
||||
fn complete(
|
||||
fn stream_completion(
|
||||
&self,
|
||||
_request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
|
||||
@@ -91,7 +91,7 @@ impl LanguageModelCompletionProvider for OllamaCompletionProvider {
|
||||
async move { Ok(token_count) }.boxed()
|
||||
}
|
||||
|
||||
fn complete(
|
||||
fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
|
||||
@@ -179,7 +179,7 @@ impl LanguageModelCompletionProvider for OpenAiCompletionProvider {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
|
||||
fn complete(
|
||||
fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,27 +9,35 @@ use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use editor::{
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
display_map::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
|
||||
BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
ToDisplayPoint,
|
||||
},
|
||||
Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
|
||||
ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use futures::{
|
||||
channel::mpsc,
|
||||
future::LocalBoxFuture,
|
||||
stream::{self, BoxStream},
|
||||
SinkExt, Stream, StreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, Global, HighlightStyle,
|
||||
Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView,
|
||||
WhiteSpace, WindowContext,
|
||||
};
|
||||
use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language::{Bias, Buffer, Point, Selection, TransactionId};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use similar::TextDiff;
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
cmp, mem,
|
||||
cmp,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::{Range, RangeInclusive},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
@@ -79,6 +87,7 @@ impl InlineAssistant {
|
||||
editor: &View<Editor>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
assistant_panel: Option<&View<AssistantPanel>>,
|
||||
initial_prompt: Option<String>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
@@ -130,11 +139,11 @@ impl InlineAssistant {
|
||||
}
|
||||
|
||||
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||
let prompt_buffer = cx.new_model(|cx| Buffer::local("", cx));
|
||||
let prompt_buffer =
|
||||
cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx));
|
||||
let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
|
||||
|
||||
let mut assists = Vec::new();
|
||||
let mut assist_blocks = Vec::new();
|
||||
let mut assist_to_focus = None;
|
||||
for range in codegen_ranges {
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
@@ -142,6 +151,7 @@ impl InlineAssistant {
|
||||
Codegen::new(
|
||||
editor.read(cx).buffer().clone(),
|
||||
range.clone(),
|
||||
false,
|
||||
self.telemetry.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -174,42 +184,18 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
assist_blocks.push(BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
position: range.start,
|
||||
height: prompt_editor.read(cx).height_in_lines,
|
||||
render: build_assist_editor_renderer(&prompt_editor),
|
||||
disposition: BlockDisposition::Above,
|
||||
});
|
||||
assist_blocks.push(BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
position: range.end,
|
||||
height: 1,
|
||||
render: Box::new(|cx| {
|
||||
v_flex()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.into_any_element()
|
||||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
});
|
||||
assists.push((assist_id, prompt_editor));
|
||||
}
|
||||
let [prompt_block_id, end_block_id] =
|
||||
self.insert_assist_blocks(editor, range, &prompt_editor, cx);
|
||||
|
||||
let assist_block_ids = editor.update(cx, |editor, cx| {
|
||||
editor.insert_blocks(assist_blocks, None, cx)
|
||||
});
|
||||
assists.push((assist_id, prompt_editor, prompt_block_id, end_block_id));
|
||||
}
|
||||
|
||||
let editor_assists = self
|
||||
.assists_by_editor
|
||||
.entry(editor.downgrade())
|
||||
.or_insert_with(|| EditorInlineAssists::new(&editor, cx));
|
||||
let mut assist_group = InlineAssistGroup::new();
|
||||
for ((assist_id, prompt_editor), block_ids) in
|
||||
assists.into_iter().zip(assist_block_ids.chunks_exact(2))
|
||||
{
|
||||
for (assist_id, prompt_editor, prompt_block_id, end_block_id) in assists {
|
||||
self.assists.insert(
|
||||
assist_id,
|
||||
InlineAssist::new(
|
||||
@@ -218,8 +204,8 @@ impl InlineAssistant {
|
||||
assistant_panel.is_some(),
|
||||
editor,
|
||||
&prompt_editor,
|
||||
block_ids[0],
|
||||
block_ids[1],
|
||||
prompt_block_id,
|
||||
end_block_id,
|
||||
prompt_editor.read(cx).codegen.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
@@ -235,6 +221,119 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn suggest_assist(
|
||||
&mut self,
|
||||
editor: &View<Editor>,
|
||||
range: Range<Anchor>,
|
||||
initial_prompt: String,
|
||||
insert_newline: bool,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
assistant_panel: Option<&View<AssistantPanel>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> InlineAssistId {
|
||||
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||
let prompt_buffer = cx.new_model(|cx| Buffer::local(&initial_prompt, cx));
|
||||
let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
|
||||
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
editor.read(cx).buffer().clone(),
|
||||
range.clone(),
|
||||
insert_newline,
|
||||
self.telemetry.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
PromptEditor::new(
|
||||
assist_id,
|
||||
gutter_dimensions.clone(),
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
codegen.clone(),
|
||||
editor,
|
||||
assistant_panel,
|
||||
workspace.clone(),
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let [prompt_block_id, end_block_id] =
|
||||
self.insert_assist_blocks(editor, range, &prompt_editor, cx);
|
||||
|
||||
let editor_assists = self
|
||||
.assists_by_editor
|
||||
.entry(editor.downgrade())
|
||||
.or_insert_with(|| EditorInlineAssists::new(&editor, cx));
|
||||
|
||||
let mut assist_group = InlineAssistGroup::new();
|
||||
self.assists.insert(
|
||||
assist_id,
|
||||
InlineAssist::new(
|
||||
assist_id,
|
||||
assist_group_id,
|
||||
assistant_panel.is_some(),
|
||||
editor,
|
||||
&prompt_editor,
|
||||
prompt_block_id,
|
||||
end_block_id,
|
||||
prompt_editor.read(cx).codegen.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
assist_group.assist_ids.push(assist_id);
|
||||
editor_assists.assist_ids.push(assist_id);
|
||||
self.assist_groups.insert(assist_group_id, assist_group);
|
||||
assist_id
|
||||
}
|
||||
|
||||
fn insert_assist_blocks(
|
||||
&self,
|
||||
editor: &View<Editor>,
|
||||
mut range: Range<Anchor>,
|
||||
prompt_editor: &View<PromptEditor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> [CustomBlockId; 2] {
|
||||
let buffer = editor.read(cx).buffer();
|
||||
range.start = range.start.bias_left(&buffer.read(cx).read(cx));
|
||||
range.end = range.end.bias_right(&buffer.read(cx).read(cx));
|
||||
let assist_blocks = vec![
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
position: range.start,
|
||||
height: prompt_editor.read(cx).height_in_lines,
|
||||
render: build_assist_editor_renderer(prompt_editor),
|
||||
disposition: BlockDisposition::Above,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
position: range.end,
|
||||
height: 1,
|
||||
render: Box::new(|cx| {
|
||||
v_flex()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.into_any_element()
|
||||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
},
|
||||
];
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let block_ids = editor.insert_blocks(assist_blocks, None, cx);
|
||||
[block_ids[0], block_ids[1]]
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||
let assist = &self.assists[&assist_id];
|
||||
let Some(decorations) = assist.decorations.as_ref() else {
|
||||
@@ -379,6 +478,14 @@ impl InlineAssistant {
|
||||
cx.propagate();
|
||||
}
|
||||
|
||||
fn handle_editor_release(&mut self, editor: WeakView<Editor>, cx: &mut WindowContext) {
|
||||
if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor) {
|
||||
for assist_id in editor_assists.assist_ids.clone() {
|
||||
self.finish_assist(assist_id, true, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_editor_change(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
|
||||
let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
|
||||
return;
|
||||
@@ -698,7 +805,7 @@ impl InlineAssistant {
|
||||
assist_group.assist_ids.clone()
|
||||
}
|
||||
|
||||
fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||
pub fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
|
||||
assist
|
||||
} else {
|
||||
@@ -727,16 +834,26 @@ impl InlineAssistant {
|
||||
self.prompt_history.pop_front();
|
||||
}
|
||||
|
||||
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
|
||||
let codegen = assist.codegen.clone();
|
||||
let request = self.request_for_inline_assist(assist_id, cx);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let request = request.await?;
|
||||
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let telemetry_id = CompletionProvider::global(cx).model().telemetry_id();
|
||||
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(stream::empty().boxed()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.request_for_inline_assist(assist_id, cx);
|
||||
let mut cx = cx.to_async();
|
||||
async move {
|
||||
let request = request.await?;
|
||||
let chunks = cx
|
||||
.update(|cx| CompletionProvider::global(cx).stream_completion(request, cx))?
|
||||
.await?;
|
||||
Ok(chunks.boxed())
|
||||
}
|
||||
.boxed_local()
|
||||
};
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.start(telemetry_id, chunks, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn request_for_inline_assist(
|
||||
@@ -855,7 +972,7 @@ impl InlineAssistant {
|
||||
})
|
||||
}
|
||||
|
||||
fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
|
||||
assist
|
||||
} else {
|
||||
@@ -1074,6 +1191,14 @@ impl EditorInlineAssists {
|
||||
}
|
||||
}),
|
||||
_subscriptions: vec![
|
||||
cx.observe_release(editor, {
|
||||
let editor = editor.downgrade();
|
||||
|_, cx| {
|
||||
InlineAssistant::update_global(cx, |this, cx| {
|
||||
this.handle_editor_release(editor, cx);
|
||||
})
|
||||
}
|
||||
}),
|
||||
cx.observe(editor, move |editor, cx| {
|
||||
InlineAssistant::update_global(cx, |this, cx| {
|
||||
this.handle_editor_change(editor, cx)
|
||||
@@ -1138,7 +1263,7 @@ fn build_assist_editor_renderer(editor: &View<PromptEditor>) -> RenderBlock {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
|
||||
struct InlineAssistId(usize);
|
||||
pub struct InlineAssistId(usize);
|
||||
|
||||
impl InlineAssistId {
|
||||
fn post_inc(&mut self) -> InlineAssistId {
|
||||
@@ -1768,8 +1893,8 @@ impl InlineAssist {
|
||||
include_context: bool,
|
||||
editor: &View<Editor>,
|
||||
prompt_editor: &View<PromptEditor>,
|
||||
prompt_block_id: BlockId,
|
||||
end_block_id: BlockId,
|
||||
prompt_block_id: CustomBlockId,
|
||||
end_block_id: CustomBlockId,
|
||||
codegen: Model<Codegen>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
@@ -1863,10 +1988,10 @@ impl InlineAssist {
|
||||
}
|
||||
|
||||
struct InlineAssistDecorations {
|
||||
prompt_block_id: BlockId,
|
||||
prompt_block_id: CustomBlockId,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
removed_line_block_ids: HashSet<BlockId>,
|
||||
end_block_id: BlockId,
|
||||
removed_line_block_ids: HashSet<CustomBlockId>,
|
||||
end_block_id: CustomBlockId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1882,6 +2007,7 @@ pub struct Codegen {
|
||||
range: Range<Anchor>,
|
||||
edit_position: Anchor,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
insert_newline: Option<Bias>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
status: CodegenStatus,
|
||||
generation: Task<()>,
|
||||
@@ -1911,6 +2037,8 @@ impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
/// If Some, insert an initial newline before generating (before or after the generation range).
|
||||
insert_newline: Option<Bias>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
@@ -1943,7 +2071,8 @@ impl Codegen {
|
||||
range,
|
||||
snapshot,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
insert_newline,
|
||||
transaction_id: None,
|
||||
status: CodegenStatus::Idle,
|
||||
generation: Task::ready(()),
|
||||
diff: Diff::default(),
|
||||
@@ -1971,8 +2100,26 @@ impl Codegen {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range.clone();
|
||||
pub fn start(
|
||||
&mut self,
|
||||
telemetry_id: String,
|
||||
stream: impl 'static + Future<Output = Result<BoxStream<'static, Result<String>>>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.undo(cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
if self.insert_newline {
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit([(self.range.start..self.range.start, "\n")], None, cx);
|
||||
self.transaction_id = buffer.end_transaction(cx);
|
||||
self.snapshot = buffer.snapshot(cx);
|
||||
}
|
||||
});
|
||||
|
||||
let mut range = self.range.clone();
|
||||
range.start = range.start.bias_left(&self.snapshot);
|
||||
range.end = range.end.bias_right(&self.snapshot);
|
||||
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
@@ -1985,15 +2132,13 @@ impl Codegen {
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
|
||||
|
||||
let model_telemetry_id = prompt.model.telemetry_id();
|
||||
let response = CompletionProvider::global(cx).complete(prompt, cx);
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.edit_position = range.start;
|
||||
self.diff = Diff::default();
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = response.await;
|
||||
let chunks = stream.await;
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
@@ -2003,7 +2148,7 @@ impl Codegen {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = StripInvalidSpans::new(response.inner.await?);
|
||||
let chunks = StripInvalidSpans::new(chunks?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
@@ -2086,7 +2231,7 @@ impl Codegen {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
model_telemetry_id,
|
||||
telemetry_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
@@ -2451,11 +2596,8 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::FakeCompletionProvider;
|
||||
|
||||
use super::*;
|
||||
use crate::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
@@ -2466,6 +2608,7 @@ mod tests {
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
use std::{future, sync::Arc};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
@@ -2475,7 +2618,7 @@ mod tests {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
@@ -2493,14 +2636,17 @@ mod tests {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx));
|
||||
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, false, None, cx));
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.start(LanguageModelRequest::default(), cx)
|
||||
codegen.start(
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
@@ -2511,11 +2657,11 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion(&LanguageModelRequest::default());
|
||||
drop(chunks_tx);
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
@@ -2536,7 +2682,6 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
@@ -2552,10 +2697,16 @@ mod tests {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx));
|
||||
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, false, None, cx));
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.start(
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
@@ -2569,11 +2720,11 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion(&LanguageModelRequest::default());
|
||||
drop(chunks_tx);
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
@@ -2594,7 +2745,7 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
@@ -2610,10 +2761,16 @@ mod tests {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx));
|
||||
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, false, None, cx));
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.start(
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
@@ -2627,11 +2784,11 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion(&LanguageModelRequest::default());
|
||||
drop(chunks_tx);
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assets::Assets;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
@@ -12,8 +13,8 @@ use futures::{
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter,
|
||||
Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
|
||||
actions, point, size, transparent_black, AppContext, AssetSource, BackgroundExecutor, Bounds,
|
||||
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
|
||||
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||
@@ -268,7 +269,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
.flex_none()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.mx_2()
|
||||
.mx_1()
|
||||
.child(editor.clone())
|
||||
}
|
||||
}
|
||||
@@ -627,7 +628,7 @@ impl PromptLibrary {
|
||||
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
||||
}
|
||||
|
||||
pub fn inline_assist(&mut self, _: &InlineAssist, cx: &mut ViewContext<Self>) {
|
||||
pub fn inline_assist(&mut self, action: &InlineAssist, cx: &mut ViewContext<Self>) {
|
||||
let Some(active_prompt_id) = self.active_prompt_id else {
|
||||
cx.propagate();
|
||||
return;
|
||||
@@ -635,9 +636,10 @@ impl PromptLibrary {
|
||||
|
||||
let prompt_editor = &self.prompt_editors[&active_prompt_id].body_editor;
|
||||
let provider = CompletionProvider::global(cx);
|
||||
let initial_prompt = action.prompt.clone();
|
||||
if provider.is_authenticated() {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, None, cx)
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
|
||||
})
|
||||
} else {
|
||||
for window in cx.windows() {
|
||||
@@ -766,6 +768,7 @@ impl PromptLibrary {
|
||||
.capture_action(cx.listener(Self::focus_active_prompt))
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.h_full()
|
||||
.px_1()
|
||||
.w_1_3()
|
||||
.overflow_x_hidden()
|
||||
.child(
|
||||
@@ -1295,6 +1298,17 @@ impl PromptStore {
|
||||
fn first(&self) -> Option<PromptMetadata> {
|
||||
self.metadata_cache.read().metadata.first().cloned()
|
||||
}
|
||||
|
||||
pub fn operations_prompt(&self) -> String {
|
||||
String::from_utf8(
|
||||
Assets
|
||||
.load("prompts/operations.md")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.to_vec(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a shared future to a prompt store so it can be assigned as a context global.
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
use language::Rope;
|
||||
use std::ops::Range;
|
||||
|
||||
/// Search the given buffer for the given substring, ignoring any differences
|
||||
/// in line indentation between the query and the buffer.
|
||||
///
|
||||
/// Returns a vector of ranges of byte offsets in the buffer corresponding
|
||||
/// to the entire lines of the buffer.
|
||||
pub fn fuzzy_search_lines(haystack: &Rope, needle: &str) -> Option<Range<usize>> {
|
||||
const SIMILARITY_THRESHOLD: f64 = 0.8;
|
||||
|
||||
let mut best_match: Option<(Range<usize>, f64)> = None; // (range, score)
|
||||
let mut haystack_lines = haystack.chunks().lines();
|
||||
let mut haystack_line_start = 0;
|
||||
while let Some(mut haystack_line) = haystack_lines.next() {
|
||||
let next_haystack_line_start = haystack_line_start + haystack_line.len() + 1;
|
||||
let mut advanced_to_next_haystack_line = false;
|
||||
|
||||
let mut matched = true;
|
||||
let match_start = haystack_line_start;
|
||||
let mut match_end = next_haystack_line_start;
|
||||
let mut match_score = 0.0;
|
||||
let mut needle_lines = needle.lines().peekable();
|
||||
while let Some(needle_line) = needle_lines.next() {
|
||||
let similarity = line_similarity(haystack_line, needle_line);
|
||||
if similarity >= SIMILARITY_THRESHOLD {
|
||||
match_end = haystack_lines.offset();
|
||||
match_score += similarity;
|
||||
|
||||
if needle_lines.peek().is_some() {
|
||||
if let Some(next_haystack_line) = haystack_lines.next() {
|
||||
advanced_to_next_haystack_line = true;
|
||||
haystack_line = next_haystack_line;
|
||||
} else {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if matched
|
||||
&& best_match
|
||||
.as_ref()
|
||||
.map(|(_, best_score)| match_score > *best_score)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
best_match = Some((match_start..match_end, match_score));
|
||||
}
|
||||
|
||||
if advanced_to_next_haystack_line {
|
||||
haystack_lines.seek(next_haystack_line_start);
|
||||
}
|
||||
haystack_line_start = next_haystack_line_start;
|
||||
}
|
||||
|
||||
best_match.map(|(range, _)| range)
|
||||
}
|
||||
|
||||
/// Calculates the similarity between two lines, ignoring leading and trailing whitespace,
|
||||
/// using the Jaro-Winkler distance.
|
||||
///
|
||||
/// Returns a value between 0.0 and 1.0, where 1.0 indicates an exact match.
|
||||
fn line_similarity(line1: &str, line2: &str) -> f64 {
|
||||
strsim::jaro_winkler(line1.trim(), line2.trim())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{AppContext, Context as _};
|
||||
use language::Buffer;
|
||||
use unindent::Unindent as _;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_fuzzy_search_lines(cx: &mut AppContext) {
|
||||
let (text, expected_ranges) = marked_text_ranges(
|
||||
&r#"
|
||||
fn main() {
|
||||
if a() {
|
||||
assert_eq!(
|
||||
1 + 2,
|
||||
does_not_match,
|
||||
);
|
||||
}
|
||||
|
||||
println!("hi");
|
||||
|
||||
assert_eq!(
|
||||
1 + 2,
|
||||
3,
|
||||
); // this last line does not match
|
||||
|
||||
« assert_eq!(
|
||||
1 + 2,
|
||||
3,
|
||||
);
|
||||
»
|
||||
|
||||
« assert_eq!(
|
||||
"something",
|
||||
"else",
|
||||
);
|
||||
»
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
false,
|
||||
);
|
||||
|
||||
let buffer = cx.new_model(|cx| Buffer::local(&text, cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let actual_range = fuzzy_search_lines(
|
||||
snapshot.as_rope(),
|
||||
&"
|
||||
assert_eq!(
|
||||
1 + 2,
|
||||
3,
|
||||
);
|
||||
"
|
||||
.unindent(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual_range, expected_ranges[0]);
|
||||
|
||||
let actual_range = fuzzy_search_lines(
|
||||
snapshot.as_rope(),
|
||||
&"
|
||||
assert_eq!(
|
||||
1 + 2,
|
||||
3,
|
||||
);
|
||||
"
|
||||
.unindent(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual_range, expected_ranges[0]);
|
||||
|
||||
let actual_range = fuzzy_search_lines(
|
||||
snapshot.as_rope(),
|
||||
&"
|
||||
asst_eq!(
|
||||
\"something\",
|
||||
\"els\"
|
||||
)
|
||||
"
|
||||
.unindent(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual_range, expected_ranges[1]);
|
||||
|
||||
let actual_range = fuzzy_search_lines(
|
||||
snapshot.as_rope(),
|
||||
&"
|
||||
assert_eq!(
|
||||
2 + 1,
|
||||
3,
|
||||
);
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
assert_eq!(actual_range, None);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ pub mod now_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
pub mod search_command;
|
||||
pub mod symbols_command;
|
||||
pub mod tabs_command;
|
||||
pub mod term_command;
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView};
|
||||
use indexed_docs::{
|
||||
IndexedDocsRegistry, IndexedDocsStore, LocalProvider, PackageName, ProviderId, RustdocIndexer,
|
||||
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
|
||||
ProviderId,
|
||||
};
|
||||
use language::LspAdapterDelegate;
|
||||
use project::{Project, ProjectPath};
|
||||
@@ -34,22 +36,22 @@ impl DocsSlashCommand {
|
||||
))
|
||||
}
|
||||
|
||||
/// Ensures that the rustdoc provider is registered.
|
||||
/// Ensures that the indexed doc providers for Rust are registered.
|
||||
///
|
||||
/// Ideally we would do this sooner, but we need to wait until we're able to
|
||||
/// access the workspace so we can read the project.
|
||||
fn ensure_rustdoc_provider_is_registered(
|
||||
fn ensure_rust_doc_providers_are_registered(
|
||||
&self,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
if indexed_docs_registry
|
||||
.get_provider_store(ProviderId::rustdoc())
|
||||
.get_provider_store(LocalRustdocProvider::id())
|
||||
.is_none()
|
||||
{
|
||||
let index_provider_deps = maybe!({
|
||||
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace.clone().ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
@@ -63,11 +65,80 @@ impl DocsSlashCommand {
|
||||
});
|
||||
|
||||
if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
|
||||
indexed_docs_registry.register_provider(Box::new(RustdocIndexer::new(Box::new(
|
||||
LocalProvider::new(fs, cargo_workspace_root),
|
||||
))));
|
||||
indexed_docs_registry.register_provider(Box::new(LocalRustdocProvider::new(
|
||||
fs,
|
||||
cargo_workspace_root,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if indexed_docs_registry
|
||||
.get_provider_store(DocsDotRsProvider::id())
|
||||
.is_none()
|
||||
{
|
||||
let http_client = maybe!({
|
||||
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
let project = workspace.read(cx).project().clone();
|
||||
anyhow::Ok(project.read(cx).client().http_client().clone())
|
||||
});
|
||||
|
||||
if let Some(http_client) = http_client.log_err() {
|
||||
indexed_docs_registry
|
||||
.register_provider(Box::new(DocsDotRsProvider::new(http_client)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs just-in-time indexing for a given package, in case the slash command
|
||||
/// is run without any entries existing in the index.
|
||||
fn run_just_in_time_indexing(
|
||||
store: Arc<IndexedDocsStore>,
|
||||
key: String,
|
||||
package: PackageName,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Task<()> {
|
||||
executor.clone().spawn(async move {
|
||||
let (prefix, needs_full_index) = if let Some((prefix, _)) = key.split_once('*') {
|
||||
// If we have a wildcard in the search, we want to wait until
|
||||
// we've completely finished indexing so we get a full set of
|
||||
// results for the wildcard.
|
||||
(prefix.to_string(), true)
|
||||
} else {
|
||||
(key, false)
|
||||
};
|
||||
|
||||
// If we already have some entries, we assume that we've indexed the package before
|
||||
// and don't need to do it again.
|
||||
let has_any_entries = store
|
||||
.any_with_prefix(prefix.clone())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if has_any_entries {
|
||||
return ();
|
||||
};
|
||||
|
||||
let index_task = store.clone().index(package.clone());
|
||||
|
||||
if needs_full_index {
|
||||
_ = index_task.await;
|
||||
} else {
|
||||
loop {
|
||||
executor.timer(Duration::from_millis(200)).await;
|
||||
|
||||
if store
|
||||
.any_with_prefix(prefix.clone())
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
|| !store.is_indexing(&package)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +166,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
self.ensure_rustdoc_provider_is_registered(workspace, cx);
|
||||
self.ensure_rust_doc_providers_are_registered(workspace, cx);
|
||||
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
let args = DocsSlashCommandArgs::parse(&query);
|
||||
@@ -121,6 +192,14 @@ impl SlashCommand for DocsSlashCommand {
|
||||
match args {
|
||||
DocsSlashCommandArgs::NoProvider => {
|
||||
let providers = indexed_docs_registry.list_providers();
|
||||
if providers.is_empty() {
|
||||
return Ok(vec![ArgumentCompletion {
|
||||
label: "No available docs providers.".to_string(),
|
||||
new_text: String::new(),
|
||||
run_command: false,
|
||||
}]);
|
||||
}
|
||||
|
||||
Ok(providers
|
||||
.into_iter()
|
||||
.map(|provider| ArgumentCompletion {
|
||||
@@ -171,46 +250,72 @@ impl SlashCommand for DocsSlashCommand {
|
||||
};
|
||||
|
||||
let args = DocsSlashCommandArgs::parse(argument);
|
||||
let text = cx.background_executor().spawn({
|
||||
let executor = cx.background_executor().clone();
|
||||
let task = cx.background_executor().spawn({
|
||||
let store = args
|
||||
.provider()
|
||||
.ok_or_else(|| anyhow!("no docs provider specified"))
|
||||
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
|
||||
async move {
|
||||
match args {
|
||||
let (provider, key) = match args.clone() {
|
||||
DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"),
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
provider, package, ..
|
||||
} => {
|
||||
let store = store?;
|
||||
let item_docs = store.load(package.clone()).await?;
|
||||
|
||||
anyhow::Ok((provider, package, item_docs.to_string()))
|
||||
}
|
||||
} => (provider, package),
|
||||
DocsSlashCommandArgs::SearchItemDocs {
|
||||
provider,
|
||||
item_path,
|
||||
..
|
||||
} => {
|
||||
let store = store?;
|
||||
let item_docs = store.load(item_path.clone()).await?;
|
||||
} => (provider, item_path),
|
||||
};
|
||||
|
||||
anyhow::Ok((provider, item_path, item_docs.to_string()))
|
||||
}
|
||||
let store = store?;
|
||||
|
||||
if let Some(package) = args.package() {
|
||||
Self::run_just_in_time_indexing(store.clone(), key.clone(), package, executor)
|
||||
.await;
|
||||
}
|
||||
|
||||
let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') {
|
||||
let docs = store.load_many_by_prefix(prefix.to_string()).await?;
|
||||
|
||||
let mut text = String::new();
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
for (key, docs) in docs {
|
||||
let prev_len = text.len();
|
||||
|
||||
text.push_str(&docs.0);
|
||||
text.push_str("\n");
|
||||
ranges.push((key, prev_len..text.len()));
|
||||
text.push_str("\n");
|
||||
}
|
||||
|
||||
(text, ranges)
|
||||
} else {
|
||||
let item_docs = store.load(key.clone()).await?;
|
||||
let text = item_docs.to_string();
|
||||
let range = 0..text.len();
|
||||
|
||||
(text, vec![(key, range)])
|
||||
};
|
||||
|
||||
anyhow::Ok((provider, text, ranges))
|
||||
}
|
||||
});
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let (provider, path, text) = text.await?;
|
||||
let range = 0..text.len();
|
||||
let (provider, text, ranges) = task.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::FileRust,
|
||||
label: format!("docs ({provider}): {path}",).into(),
|
||||
}],
|
||||
sections: ranges
|
||||
.into_iter()
|
||||
.map(|(key, range)| SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::FileDoc,
|
||||
label: format!("docs ({provider}): {key}",).into(),
|
||||
})
|
||||
.collect(),
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
@@ -221,7 +326,7 @@ fn is_item_path_delimiter(char: char) -> bool {
|
||||
!char.is_alphanumeric() && char != '-' && char != '_'
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub(crate) enum DocsSlashCommandArgs {
|
||||
NoProvider,
|
||||
SearchPackageDocs {
|
||||
|
||||
@@ -27,7 +27,7 @@ pub(crate) struct FetchSlashCommand;
|
||||
impl FetchSlashCommand {
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") {
|
||||
if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ impl SlashCommand for NowSlashCommand {
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert current date and time".into()
|
||||
"Insert Current Date and Time".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
89
crates/assistant/src/slash_command/symbols_command.rs
Normal file
89
crates/assistant/src/slash_command/symbols_command.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::atomic::AtomicBool};
|
||||
use ui::{IconName, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct OutlineSlashCommand;
|
||||
|
||||
impl SlashCommand for OutlineSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"symbols".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert symbols for active tab".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Symbols for Active Tab".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let Some(active_item) = workspace.active_item(cx) else {
|
||||
return Task::ready(Err(anyhow!("no active tab")));
|
||||
};
|
||||
let Some(buffer) = active_item
|
||||
.downcast::<Editor>()
|
||||
.and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("active tab is not an editor")));
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path = snapshot.resolve_file_path(cx, true);
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let outline = snapshot
|
||||
.outline(None)
|
||||
.context("no symbols for active tab")?;
|
||||
|
||||
let path = path.as_deref().unwrap_or(Path::new("untitled"));
|
||||
let mut outline_text = format!("Symbols for {}:\n", path.display());
|
||||
for item in &outline.path_candidates {
|
||||
outline_text.push_str("- ");
|
||||
outline_text.push_str(&item.string);
|
||||
outline_text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..outline_text.len(),
|
||||
icon: IconName::ListTree,
|
||||
label: path.to_string_lossy().to_string().into(),
|
||||
}],
|
||||
text: outline_text,
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
output.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ impl SlashCommand for TermSlashCommand {
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert terminal output".into()
|
||||
"Insert Terminal Output".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -73,11 +73,13 @@ impl TerminalInlineAssistant {
|
||||
terminal_view: &View<TerminalView>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
assistant_panel: Option<&View<AssistantPanel>>,
|
||||
initial_prompt: Option<String>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let terminal = terminal_view.read(cx).terminal().clone();
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let prompt_buffer = cx.new_model(|cx| Buffer::local("", cx));
|
||||
let prompt_buffer =
|
||||
cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx));
|
||||
let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
|
||||
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
@@ -1026,7 +1028,7 @@ impl Codegen {
|
||||
|
||||
let telemetry = self.telemetry.clone();
|
||||
let model_telemetry_id = prompt.model.telemetry_id();
|
||||
let response = CompletionProvider::global(cx).complete(prompt, cx);
|
||||
let response = CompletionProvider::global(cx).stream_completion(prompt, cx);
|
||||
|
||||
self.generation = cx.spawn(|this, mut cx| async move {
|
||||
let response = response.await;
|
||||
@@ -1037,8 +1039,8 @@ impl Codegen {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let task = async {
|
||||
let mut response = response.inner.await?;
|
||||
while let Some(chunk) = response.next().await {
|
||||
let mut chunks = response?;
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH};
|
||||
use client::{Client, TelemetrySettings};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
@@ -288,7 +288,12 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(Box::new(view.clone()), None, cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(view.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
@@ -448,12 +453,7 @@ 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 {
|
||||
let binary_path = match OS {
|
||||
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
|
||||
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
|
||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||
@@ -536,9 +536,10 @@ async fn install_release_linux(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
downloaded_tar_gz: PathBuf,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
) -> Result<PathBuf> {
|
||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
||||
let running_app_path = cx.update(|cx| cx.app_path())??;
|
||||
|
||||
let extracted = temp_dir.path().join("zed");
|
||||
fs::create_dir_all(&extracted)
|
||||
@@ -569,7 +570,16 @@ async fn install_release_linux(
|
||||
let app_folder_name = format!("zed{}.app", suffix);
|
||||
|
||||
let from = extracted.join(&app_folder_name);
|
||||
let to = home_dir.join(".local");
|
||||
let mut to = home_dir.join(".local");
|
||||
|
||||
let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
|
||||
|
||||
if let Some(prefix) = running_app_path
|
||||
.to_str()
|
||||
.and_then(|str| str.strip_suffix(&expected_suffix))
|
||||
{
|
||||
to = PathBuf::from(prefix);
|
||||
}
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
@@ -586,17 +596,15 @@ async fn install_release_linux(
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
Ok(to.join(expected_suffix))
|
||||
}
|
||||
|
||||
async fn install_release_macos(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
downloaded_dmg: PathBuf,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let running_app_path = ZED_APP_PATH
|
||||
.clone()
|
||||
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
|
||||
) -> Result<PathBuf> {
|
||||
let running_app_path = cx.update(|cx| cx.app_path())??;
|
||||
let running_app_filename = running_app_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid running app path"))?;
|
||||
@@ -644,5 +652,5 @@ async fn install_release_macos(
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
Ok(running_app_path)
|
||||
}
|
||||
|
||||
@@ -113,29 +113,30 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_item = None;
|
||||
if let Some(item) = active_pane_item {
|
||||
let this = cx.view().downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.notify();
|
||||
if let Some(active_item) = this.active_item.as_ref() {
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
active_item.breadcrumb_location(cx),
|
||||
))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
));
|
||||
self.active_item = Some(item.boxed_clone());
|
||||
item.breadcrumb_location(cx)
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
|
||||
let Some(item) = active_pane_item else {
|
||||
return ToolbarItemLocation::Hidden;
|
||||
};
|
||||
|
||||
let this = cx.view().downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.notify();
|
||||
if let Some(active_item) = this.active_item.as_ref() {
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
active_item.breadcrumb_location(cx),
|
||||
))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
));
|
||||
self.active_item = Some(item.boxed_clone());
|
||||
item.breadcrumb_location(cx)
|
||||
}
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct IpcHandshake {
|
||||
pub enum CliRequest {
|
||||
Open {
|
||||
paths: Vec<String>,
|
||||
urls: Vec<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
dev_server_token: Option<String>,
|
||||
|
||||
@@ -5,6 +5,7 @@ use clap::Parser;
|
||||
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitStatus,
|
||||
@@ -37,8 +38,7 @@ struct Args {
|
||||
///
|
||||
/// Use `path:line:row` syntax to open a file at a specific location.
|
||||
/// Non-existing paths and directories will ignore `:line:row` suffix.
|
||||
#[arg(value_parser = parse_path_with_position)]
|
||||
paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
|
||||
paths_with_position: Vec<String>,
|
||||
/// Print Zed's version and the app path.
|
||||
#[arg(short, long)]
|
||||
version: bool,
|
||||
@@ -53,12 +53,30 @@ struct Args {
|
||||
dev_server_token: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_path_with_position(
|
||||
argument_str: &str,
|
||||
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
|
||||
PathLikeWithPosition::parse_str(argument_str, |_, path_str| {
|
||||
fn parse_path_with_position(argument_str: &str) -> Result<String, std::io::Error> {
|
||||
let path_like = PathLikeWithPosition::parse_str::<Infallible>(argument_str, |_, path_str| {
|
||||
Ok(Path::new(path_str).to_path_buf())
|
||||
})
|
||||
.unwrap();
|
||||
let curdir = env::current_dir()?;
|
||||
|
||||
let canonicalized = path_like.map_path_like(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(canonicalized.to_string(|path| path.display().to_string()))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
@@ -91,28 +109,6 @@ fn main() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let curdir = env::current_dir()?;
|
||||
let mut paths = vec![];
|
||||
for path in args.paths_with_position {
|
||||
let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir;
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
paths.push(canonicalized.to_string(|path| path.display().to_string()))
|
||||
}
|
||||
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
@@ -126,6 +122,19 @@ fn main() -> Result<()> {
|
||||
};
|
||||
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
let mut paths = vec![];
|
||||
let mut urls = vec![];
|
||||
for path in args.paths_with_position.iter() {
|
||||
if path.starts_with("zed://")
|
||||
|| path.starts_with("http://")
|
||||
|| path.starts_with("https://")
|
||||
|| path.starts_with("file://")
|
||||
{
|
||||
urls.push(path.to_string());
|
||||
} else {
|
||||
paths.push(parse_path_with_position(path)?)
|
||||
}
|
||||
}
|
||||
|
||||
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
|
||||
let exit_status = exit_status.clone();
|
||||
@@ -134,6 +143,7 @@ fn main() -> Result<()> {
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
urls,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
|
||||
@@ -13,8 +13,9 @@ use async_tungstenite::tungstenite::{
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt,
|
||||
TryFutureExt as _, TryStreamExt,
|
||||
channel::oneshot,
|
||||
future::{BoxFuture, LocalBoxFuture},
|
||||
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
|
||||
@@ -23,6 +24,7 @@ use http::{HttpClient, HttpClientWithUrl};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
use proto::ProtoClient;
|
||||
use rand::prelude::*;
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
@@ -1408,6 +1410,11 @@ impl Client {
|
||||
self.peer.send(self.connection_id()?, message)
|
||||
}
|
||||
|
||||
fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
let connection_id = self.connection_id()?;
|
||||
self.peer.send_dynamic(connection_id, envelope)
|
||||
}
|
||||
|
||||
pub fn request<T: RequestMessage>(
|
||||
&self,
|
||||
request: T,
|
||||
@@ -1606,6 +1613,20 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoClient for Client {
|
||||
fn request(
|
||||
&self,
|
||||
envelope: proto::Envelope,
|
||||
request_type: &'static str,
|
||||
) -> BoxFuture<'static, Result<proto::Envelope>> {
|
||||
self.request_dynamic(envelope, request_type).boxed()
|
||||
}
|
||||
|
||||
fn send(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
self.send_dynamic(envelope)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DevelopmentCredentials {
|
||||
user_id: u64,
|
||||
|
||||
@@ -414,5 +414,5 @@ CREATE TABLE dev_servers (
|
||||
CREATE TABLE dev_server_projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||
path TEXT NOT NULL
|
||||
paths TEXT NOT NULL
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE dev_server_projects ADD COLUMN paths JSONB NULL;
|
||||
UPDATE dev_server_projects SET paths = to_json(ARRAY[path]);
|
||||
ALTER TABLE dev_server_projects ALTER COLUMN paths SET NOT NULL;
|
||||
ALTER TABLE dev_server_projects ALTER COLUMN path DROP NOT NULL;
|
||||
@@ -43,11 +43,36 @@ async fn get_extensions(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetExtensionsParams>,
|
||||
) -> Result<Json<GetExtensionsResponse>> {
|
||||
let extensions = app
|
||||
let mut extensions = app
|
||||
.db
|
||||
.get_extensions(params.filter.as_deref(), params.max_schema_version, 500)
|
||||
.await?;
|
||||
|
||||
if let Some(filter) = params.filter.as_deref() {
|
||||
let extension_id = filter.to_lowercase();
|
||||
let mut exact_match = None;
|
||||
extensions.retain(|extension| {
|
||||
if extension.id.as_ref() == &extension_id {
|
||||
exact_match = Some(extension.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
if exact_match.is_none() {
|
||||
exact_match = app
|
||||
.db
|
||||
.get_extensions_by_ids(&[&extension_id], None)
|
||||
.await?
|
||||
.first()
|
||||
.cloned();
|
||||
}
|
||||
|
||||
if let Some(exact_match) = exact_match {
|
||||
extensions.insert(0, exact_match);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(query) = params.filter.as_deref() {
|
||||
let count = extensions.len();
|
||||
tracing::info!(query, count, "extension_search")
|
||||
|
||||
@@ -5,7 +5,7 @@ use rpc::{
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
||||
ModelTrait, QueryFilter,
|
||||
IntoActiveModel, ModelTrait, QueryFilter,
|
||||
};
|
||||
|
||||
use crate::db::ProjectId;
|
||||
@@ -56,12 +56,7 @@ impl Database {
|
||||
.await?;
|
||||
Ok(servers
|
||||
.into_iter()
|
||||
.map(|(dev_server_project, project)| proto::DevServerProject {
|
||||
id: dev_server_project.id.to_proto(),
|
||||
project_id: project.map(|p| p.id.to_proto()),
|
||||
dev_server_id: dev_server_project.dev_server_id.to_proto(),
|
||||
path: dev_server_project.path,
|
||||
})
|
||||
.map(|(dev_server_project, project)| dev_server_project.to_proto(project))
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -134,7 +129,7 @@ impl Database {
|
||||
let project = dev_server_project::Entity::insert(dev_server_project::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
dev_server_id: ActiveValue::Set(dev_server_id),
|
||||
path: ActiveValue::Set(path.to_string()),
|
||||
paths: ActiveValue::Set(dev_server_project::JSONPaths(vec![path.to_string()])),
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
@@ -148,6 +143,38 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_dev_server_project(
|
||||
&self,
|
||||
id: DevServerProjectId,
|
||||
paths: &Vec<String>,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(dev_server_project::Model, proto::DevServerProjectsUpdate)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let paths = paths.clone();
|
||||
let Some((project, Some(dev_server))) = dev_server_project::Entity::find_by_id(id)
|
||||
.find_also_related(dev_server::Entity)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
else {
|
||||
return Err(anyhow!("no such dev server project"))?;
|
||||
};
|
||||
|
||||
if dev_server.user_id != user_id {
|
||||
return Err(anyhow!("not your dev server"))?;
|
||||
}
|
||||
let mut project = project.into_active_model();
|
||||
project.paths = ActiveValue::Set(dev_server_project::JSONPaths(paths));
|
||||
let project = project.update(&*tx).await?;
|
||||
|
||||
let status = self
|
||||
.dev_server_projects_update_internal(user_id, &tx)
|
||||
.await?;
|
||||
|
||||
Ok((project, status))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_dev_server_project(
|
||||
&self,
|
||||
dev_server_project_id: DevServerProjectId,
|
||||
@@ -258,7 +285,6 @@ impl Database {
|
||||
dev_server_id: DevServerId,
|
||||
connection: ConnectionId,
|
||||
) -> crate::Result<Vec<ResharedProject>> {
|
||||
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
|
||||
self.transaction(|tx| async move {
|
||||
let mut ret = Vec::new();
|
||||
for reshared_project in reshared_projects {
|
||||
@@ -322,7 +348,6 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection_id: ConnectionId,
|
||||
) -> crate::Result<Vec<RejoinedProject>> {
|
||||
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
|
||||
self.transaction(|tx| async move {
|
||||
let mut ret = Vec::new();
|
||||
for rejoined_project in rejoined_projects {
|
||||
|
||||
@@ -19,6 +19,28 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_dev_server_for_user(
|
||||
&self,
|
||||
dev_server_id: DevServerId,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<dev_server::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
let server = dev_server::Entity::find_by_id(dev_server_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("no dev server with id {}", dev_server_id))?;
|
||||
if server.user_id != user_id {
|
||||
return Err(anyhow::anyhow!(
|
||||
"dev server {} is not owned by user {}",
|
||||
dev_server_id,
|
||||
user_id
|
||||
))?;
|
||||
}
|
||||
Ok(server)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result<Vec<dev_server::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(dev_server::Entity::find()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::project;
|
||||
use crate::db::{DevServerId, DevServerProjectId};
|
||||
use rpc::proto;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{entity::prelude::*, FromJsonQueryResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "dev_server_projects")]
|
||||
@@ -9,9 +10,12 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: DevServerProjectId,
|
||||
pub dev_server_id: DevServerId,
|
||||
pub path: String,
|
||||
pub paths: JSONPaths,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
|
||||
pub struct JSONPaths(pub Vec<String>);
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -44,7 +48,12 @@ impl Model {
|
||||
id: self.id.to_proto(),
|
||||
project_id: project.map(|p| p.id.to_proto()),
|
||||
dev_server_id: self.dev_server_id.to_proto(),
|
||||
path: self.path.clone(),
|
||||
path: self.paths().get(0).cloned().unwrap_or_default(),
|
||||
paths: self.paths().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> &Vec<String> {
|
||||
&self.paths.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,11 +431,13 @@ impl Server {
|
||||
.add_request_handler(user_handler(join_hosted_project))
|
||||
.add_request_handler(user_handler(rejoin_dev_server_projects))
|
||||
.add_request_handler(user_handler(create_dev_server_project))
|
||||
.add_request_handler(user_handler(update_dev_server_project))
|
||||
.add_request_handler(user_handler(delete_dev_server_project))
|
||||
.add_request_handler(user_handler(create_dev_server))
|
||||
.add_request_handler(user_handler(regenerate_dev_server_token))
|
||||
.add_request_handler(user_handler(rename_dev_server))
|
||||
.add_request_handler(user_handler(delete_dev_server))
|
||||
.add_request_handler(user_handler(list_remote_directory))
|
||||
.add_request_handler(dev_server_handler(share_dev_server_project))
|
||||
.add_request_handler(dev_server_handler(shutdown_dev_server))
|
||||
.add_request_handler(dev_server_handler(reconnect_dev_server))
|
||||
@@ -2313,6 +2315,69 @@ async fn join_hosted_project(
|
||||
join_project_internal(response, session, &mut project, &replica_id)
|
||||
}
|
||||
|
||||
async fn list_remote_directory(
|
||||
request: proto::ListRemoteDirectory,
|
||||
response: Response<proto::ListRemoteDirectory>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||
let dev_server_connection_id = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id_supporting(dev_server_id, ZedVersion::with_list_directory())?;
|
||||
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.get_dev_server_for_user(dev_server_id, session.user_id())
|
||||
.await?;
|
||||
|
||||
response.send(
|
||||
session
|
||||
.peer
|
||||
.forward_request(session.connection_id, dev_server_connection_id, request)
|
||||
.await?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_dev_server_project(
|
||||
request: proto::UpdateDevServerProject,
|
||||
response: Response<proto::UpdateDevServerProject>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let dev_server_project_id = DevServerProjectId(request.dev_server_project_id as i32);
|
||||
|
||||
let (dev_server_project, update) = session
|
||||
.db()
|
||||
.await
|
||||
.update_dev_server_project(dev_server_project_id, &request.paths, session.user_id())
|
||||
.await?;
|
||||
|
||||
let projects = session
|
||||
.db()
|
||||
.await
|
||||
.get_projects_for_dev_server(dev_server_project.dev_server_id)
|
||||
.await?;
|
||||
|
||||
let dev_server_connection_id = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id_supporting(
|
||||
dev_server_project.dev_server_id,
|
||||
ZedVersion::with_list_directory(),
|
||||
)?;
|
||||
|
||||
session.peer.send(
|
||||
dev_server_connection_id,
|
||||
proto::DevServerInstructions { projects },
|
||||
)?;
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), update, &session).await;
|
||||
|
||||
response.send(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn create_dev_server_project(
|
||||
request: proto::CreateDevServerProject,
|
||||
response: Response<proto::CreateDevServerProject>,
|
||||
|
||||
@@ -38,6 +38,10 @@ impl ZedVersion {
|
||||
pub fn with_save_as() -> ZedVersion {
|
||||
ZedVersion(SemanticVersion::new(0, 134, 0))
|
||||
}
|
||||
|
||||
pub fn with_list_directory() -> ZedVersion {
|
||||
ZedVersion(SemanticVersion::new(0, 145, 0))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait VersionedMessage {
|
||||
@@ -187,6 +191,18 @@ impl ConnectionPool {
|
||||
self.connected_dev_servers.get(&dev_server_id).copied()
|
||||
}
|
||||
|
||||
pub fn dev_server_connection_id_supporting(
|
||||
&self,
|
||||
dev_server_id: DevServerId,
|
||||
required: ZedVersion,
|
||||
) -> Result<ConnectionId> {
|
||||
match self.connected_dev_servers.get(&dev_server_id) {
|
||||
Some(cid) if self.connections[cid].zed_version >= required => Ok(*cid),
|
||||
Some(_) => Err(anyhow!(proto::ErrorCode::RemoteUpgradeRequired)),
|
||||
None => Err(anyhow!(proto::ErrorCode::DevServerOffline)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_user_ids(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
||||
@@ -66,7 +66,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
.update(cx, |store, cx| {
|
||||
let projects = store.dev_server_projects();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
assert_eq!(projects[0].paths, vec!["/remote"]);
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
@@ -206,7 +206,7 @@ async fn create_dev_server_project(
|
||||
.update(cx, |store, cx| {
|
||||
let projects = store.dev_server_projects();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
assert_eq!(projects[0].paths, vec!["/remote"]);
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
|
||||
@@ -135,7 +135,7 @@ async fn test_basic_following(
|
||||
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
|
||||
});
|
||||
|
||||
// When client B starts following client A, all visible view states are replicated to client B.
|
||||
// When client B starts following client A, only the active view state is replicated to client B.
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
|
||||
|
||||
cx_c.executor().run_until_parked();
|
||||
@@ -156,7 +156,7 @@ async fn test_basic_following(
|
||||
);
|
||||
assert_eq!(
|
||||
editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![3..2]
|
||||
vec![3..3]
|
||||
);
|
||||
|
||||
executor.run_until_parked();
|
||||
@@ -194,7 +194,7 @@ async fn test_basic_following(
|
||||
|
||||
// Client C unfollows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.unfollow(&workspace.active_pane().clone(), cx);
|
||||
workspace.unfollow(peer_id_a, cx).unwrap();
|
||||
});
|
||||
|
||||
// All clients see that clients B is following client A.
|
||||
@@ -266,7 +266,7 @@ async fn test_basic_following(
|
||||
|
||||
// When client A activates a different editor, client B does so as well.
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_item(&editor_a1, cx)
|
||||
workspace.activate_item(&editor_a1, true, true, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
@@ -311,7 +311,7 @@ async fn test_basic_following(
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx)
|
||||
});
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
|
||||
editor
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -398,10 +398,10 @@ async fn test_basic_following(
|
||||
|
||||
// After unfollowing, client B stops receiving updates from client A.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.unfollow(&workspace.active_pane().clone(), cx)
|
||||
workspace.unfollow(peer_id_a, cx).unwrap()
|
||||
});
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_item(&editor_a2, cx)
|
||||
workspace.activate_item(&editor_a2, true, true, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
@@ -466,7 +466,7 @@ async fn test_basic_following(
|
||||
|
||||
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.activate_item(&multibuffer_editor_b, cx)
|
||||
workspace.activate_item(&multibuffer_editor_b, true, true, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
|
||||
@@ -1528,7 +1528,7 @@ async fn test_project_reconnect(
|
||||
});
|
||||
let (worktree_a2, _) = project_a1
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_local_worktree("/root-1/dir2", true, cx)
|
||||
p.find_or_create_worktree("/root-1/dir2", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1601,7 +1601,7 @@ async fn test_project_reconnect(
|
||||
});
|
||||
let (worktree_a3, _) = project_a1
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_local_worktree("/root-1/dir3", true, cx)
|
||||
p.find_or_create_worktree("/root-1/dir3", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1725,7 +1725,7 @@ async fn test_project_reconnect(
|
||||
// While client B is disconnected, add and remove worktrees from client A's project.
|
||||
let (worktree_a4, _) = project_a1
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_local_worktree("/root-1/dir4", true, cx)
|
||||
p.find_or_create_worktree("/root-1/dir4", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3327,7 +3327,7 @@ async fn test_local_settings(
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id().to_usize())
|
||||
.local_settings(worktree_b.entity_id().as_u64() as _)
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
|
||||
@@ -3346,7 +3346,7 @@ async fn test_local_settings(
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id().to_usize())
|
||||
.local_settings(worktree_b.entity_id().as_u64() as _)
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{}"#.to_string()),
|
||||
@@ -3375,7 +3375,7 @@ async fn test_local_settings(
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id().to_usize())
|
||||
.local_settings(worktree_b.entity_id().as_u64() as _)
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
@@ -3407,7 +3407,7 @@ async fn test_local_settings(
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id().to_usize())
|
||||
.local_settings(worktree_b.entity_id().as_u64() as _)
|
||||
.collect::<Vec<_>>(),
|
||||
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
|
||||
)
|
||||
@@ -4887,7 +4887,7 @@ async fn test_project_search(
|
||||
let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
|
||||
let (worktree_2, _) = project_a
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_local_worktree("/root/dir-2", true, cx)
|
||||
p.find_or_create_worktree("/root/dir-2", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -581,7 +581,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree(&new_root_path, true, cx)
|
||||
project.find_or_create_worktree(&new_root_path, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1237,7 +1237,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
}
|
||||
|
||||
for buffer in guest_project.opened_buffers() {
|
||||
for buffer in guest_project.opened_buffers(cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
assert_eq!(
|
||||
buffer.deferred_ops_len(),
|
||||
@@ -1287,8 +1287,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
for guest_buffer in guest_buffers {
|
||||
let buffer_id =
|
||||
guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
|
||||
let host_buffer = host_project.read_with(host_cx, |project, _| {
|
||||
project.buffer_for_id(buffer_id).unwrap_or_else(|| {
|
||||
let host_buffer = host_project.read_with(host_cx, |project, cx| {
|
||||
project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"host does not have buffer for guest:{}, peer:{:?}, id:{}",
|
||||
client.username,
|
||||
|
||||
@@ -805,9 +805,7 @@ impl TestClient {
|
||||
) -> (Model<Project>, WorktreeId) {
|
||||
let project = self.build_empty_local_project(cx);
|
||||
let (worktree, _) = project
|
||||
.update(cx, |p, cx| {
|
||||
p.find_or_create_local_worktree(root_path, true, cx)
|
||||
})
|
||||
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
|
||||
@@ -32,6 +32,7 @@ test-support = [
|
||||
anyhow.workspace = true
|
||||
call.workspace = true
|
||||
channel.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
|
||||
@@ -11,21 +11,22 @@ use editor::{
|
||||
EditorEvent,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter,
|
||||
FocusableView, IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, FocusableView, Model,
|
||||
Pixels, Point, Render, Subscription, Task, View, ViewContext, VisualContext as _, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use project::Project;
|
||||
use rpc::proto::ChannelVisibility;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::{prelude::*, Label};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::item::TabContentParams;
|
||||
use workspace::{item::Dedup, notifications::NotificationId};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
register_followable_item,
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
@@ -33,7 +34,7 @@ use workspace::{
|
||||
actions!(collab, [CopyLink]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
workspace::FollowableViewRegistry::register::<ChannelView>(cx)
|
||||
}
|
||||
|
||||
pub struct ChannelView {
|
||||
@@ -83,6 +84,56 @@ impl ChannelView {
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let channel_view = Self::load(channel_id, workspace, cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
|
||||
{
|
||||
if let Some(link_position) = link_position {
|
||||
existing_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link_position) = link_position {
|
||||
channel_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
|
||||
channel_view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
channel_id: ChannelId,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let workspace = workspace.read(cx);
|
||||
@@ -107,49 +158,11 @@ impl ChannelView {
|
||||
})
|
||||
})?;
|
||||
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
if let Some(link_position) = link_position {
|
||||
existing_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
let view = cx.new_view(|cx| {
|
||||
let mut this =
|
||||
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link_position) = link_position {
|
||||
view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
|
||||
view
|
||||
cx.new_view(|cx| {
|
||||
let mut this =
|
||||
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -374,24 +387,45 @@ impl Item for ChannelView {
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let label = if let Some(channel) = self.channel(cx) {
|
||||
match (
|
||||
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
|
||||
let channel = self.channel(cx)?;
|
||||
let icon = match channel.visibility {
|
||||
ChannelVisibility::Public => IconName::Public,
|
||||
ChannelVisibility::Members => IconName::Hash,
|
||||
};
|
||||
|
||||
Some(Icon::new(icon))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> gpui::AnyElement {
|
||||
let (channel_name, status) = if let Some(channel) = self.channel(cx) {
|
||||
let status = match (
|
||||
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(false, true) => format!("#{}", channel.name),
|
||||
(true, true) => format!("#{} (read-only)", channel.name),
|
||||
(_, false) => format!("#{} (disconnected)", channel.name),
|
||||
}
|
||||
(false, true) => None,
|
||||
(true, true) => Some("read-only"),
|
||||
(_, false) => Some("disconnected"),
|
||||
};
|
||||
|
||||
(channel.name.clone(), status)
|
||||
} else {
|
||||
"channel notes (disconnected)".to_string()
|
||||
("<unknown>".into(), Some("disconnected"))
|
||||
};
|
||||
Label::new(label)
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(channel_name)
|
||||
.color(params.text_color())
|
||||
.italic(params.preview),
|
||||
)
|
||||
.when_some(status, |element, status| {
|
||||
element.child(
|
||||
Label::new(status)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -478,7 +512,6 @@ impl FollowableItem for ChannelView {
|
||||
}
|
||||
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<workspace::Workspace>,
|
||||
remote_id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
@@ -491,8 +524,7 @@ impl FollowableItem for ChannelView {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let open =
|
||||
ChannelView::open_in_pane(ChannelId(state.channel_id), None, pane, workspace, cx);
|
||||
let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
|
||||
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
@@ -563,6 +595,19 @@ impl FollowableItem for ChannelView {
|
||||
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
|
||||
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
|
||||
let existing = existing.channel_buffer.read(cx);
|
||||
if self.channel_buffer.read(cx).channel_id == existing.channel_id {
|
||||
if existing.is_connected() {
|
||||
Some(Dedup::KeepExisting)
|
||||
} else {
|
||||
Some(Dedup::ReplaceExisting)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
|
||||
|
||||
@@ -111,6 +111,7 @@ impl ChatPanel {
|
||||
this.is_scrolled_to_bottom = !event.is_scrolled;
|
||||
}));
|
||||
|
||||
let local_offset = chrono::Local::now().offset().local_minus_utc();
|
||||
let mut this = Self {
|
||||
fs,
|
||||
client,
|
||||
@@ -120,7 +121,7 @@ impl ChatPanel {
|
||||
active_chat: Default::default(),
|
||||
pending_serialization: Task::ready(None),
|
||||
message_editor: input_editor,
|
||||
local_timezone: cx.local_timezone(),
|
||||
local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
|
||||
subscriptions: Vec::new(),
|
||||
is_scrolled_to_bottom: true,
|
||||
active: false,
|
||||
|
||||
@@ -5,7 +5,6 @@ use gpui::{
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
@@ -127,11 +127,12 @@ impl NotificationPanel {
|
||||
},
|
||||
));
|
||||
|
||||
let local_offset = chrono::Local::now().offset().local_minus_utc();
|
||||
let mut this = Self {
|
||||
fs,
|
||||
client,
|
||||
user_store,
|
||||
local_timezone: cx.local_timezone(),
|
||||
local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
|
||||
channel_store: ChannelStore::global(cx),
|
||||
notification_store: NotificationStore::global(cx),
|
||||
notification_list,
|
||||
|
||||
@@ -477,7 +477,7 @@ mod tests {
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
|
||||
editor.update(cx, |editor, cx| editor.focus(cx))
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct Store {
|
||||
pub struct DevServerProject {
|
||||
pub id: DevServerProjectId,
|
||||
pub project_id: Option<ProjectId>,
|
||||
pub path: SharedString,
|
||||
pub paths: Vec<SharedString>,
|
||||
pub dev_server_id: DevServerId,
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ impl From<proto::DevServerProject> for DevServerProject {
|
||||
Self {
|
||||
id: DevServerProjectId(project.id),
|
||||
project_id: project.project_id.map(|id| ProjectId(id)),
|
||||
path: project.path.into(),
|
||||
paths: project.paths.into_iter().map(|path| path.into()).collect(),
|
||||
dev_server_id: DevServerId(project.dev_server_id),
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl Store {
|
||||
.filter(|project| project.dev_server_id == id)
|
||||
.cloned()
|
||||
.collect();
|
||||
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
||||
projects.sort_by_key(|p| (p.paths.clone(), p.id));
|
||||
projects
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ impl Store {
|
||||
pub fn dev_server_projects(&self) -> Vec<DevServerProject> {
|
||||
let mut projects: Vec<DevServerProject> =
|
||||
self.dev_server_projects.values().cloned().collect();
|
||||
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
||||
projects.sort_by_key(|p| (p.paths.clone(), p.id));
|
||||
projects
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,13 @@ collections.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -4,16 +4,18 @@ mod toolbar_controls;
|
||||
|
||||
#[cfg(test)]
|
||||
mod diagnostics_tests;
|
||||
pub(crate) mod grouped_diagnostics;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeSet, HashSet};
|
||||
use editor::{
|
||||
diagnostic_block_renderer,
|
||||
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
|
||||
display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
|
||||
highlight_diagnostic_message,
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{
|
||||
channel::mpsc::{self, UnboundedSender},
|
||||
StreamExt as _,
|
||||
@@ -43,7 +45,7 @@ use ui::{h_flex, prelude::*, Icon, IconName, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
||||
ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
@@ -52,6 +54,9 @@ pub fn init(cx: &mut AppContext) {
|
||||
ProjectDiagnosticsSettings::register(cx);
|
||||
cx.observe_new_views(ProjectDiagnosticsEditor::register)
|
||||
.detach();
|
||||
if !cx.has_flag::<feature_flags::GroupedDiagnostics>() {
|
||||
grouped_diagnostics::init(cx);
|
||||
}
|
||||
}
|
||||
|
||||
struct ProjectDiagnosticsEditor {
|
||||
@@ -80,7 +85,7 @@ struct DiagnosticGroupState {
|
||||
primary_diagnostic: DiagnosticEntry<language::Anchor>,
|
||||
primary_excerpt_ix: usize,
|
||||
excerpts: Vec<ExcerptId>,
|
||||
blocks: HashSet<BlockId>,
|
||||
blocks: HashSet<CustomBlockId>,
|
||||
block_count: usize,
|
||||
}
|
||||
|
||||
@@ -232,13 +237,13 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
workspace.activate_item(&existing, true, true, cx);
|
||||
} else {
|
||||
let workspace_handle = cx.view().downgrade();
|
||||
let diagnostics = cx.new_view(|cx| {
|
||||
ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
|
||||
});
|
||||
workspace.add_item_to_active_pane(Box::new(diagnostics), None, cx);
|
||||
workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,7 +471,9 @@ impl ProjectDiagnosticsEditor {
|
||||
position: (excerpt_id, entry.range.start),
|
||||
height: diagnostic.message.matches('\n').count() as u8 + 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: diagnostic_block_renderer(diagnostic, true),
|
||||
render: diagnostic_block_renderer(
|
||||
diagnostic, None, true, true,
|
||||
),
|
||||
disposition: BlockDisposition::Below,
|
||||
});
|
||||
}
|
||||
@@ -642,11 +649,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
|
||||
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
|
||||
Label::new("No problems")
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.color(params.text_color())
|
||||
.into_any_element()
|
||||
} else {
|
||||
h_flex()
|
||||
@@ -656,13 +659,10 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new(self.summary.error_count.to_string()).color(
|
||||
if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
)),
|
||||
.child(
|
||||
Label::new(self.summary.error_count.to_string())
|
||||
.color(params.text_color()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(self.summary.warning_count > 0, |then| {
|
||||
@@ -670,13 +670,10 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
|
||||
.child(Label::new(self.summary.warning_count.to_string()).color(
|
||||
if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
)),
|
||||
.child(
|
||||
Label::new(self.summary.warning_count.to_string())
|
||||
.color(params.text_color()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
@@ -779,26 +776,12 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("diagnostics")
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
_item_id: workspace::ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
|
||||
}
|
||||
}
|
||||
|
||||
const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
|
||||
|
||||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic);
|
||||
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
|
||||
let message: SharedString = message;
|
||||
Box::new(move |cx| {
|
||||
let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
display_map::{BlockContext, DisplayRow, TransformBlock},
|
||||
display_map::{Block, BlockContext, DisplayRow},
|
||||
DisplayPoint, GutterDimensions,
|
||||
};
|
||||
use gpui::{px, AvailableSpace, Stateful, TestAppContext, VisualTestContext};
|
||||
@@ -973,10 +973,10 @@ fn editor_blocks(
|
||||
blocks.extend(
|
||||
snapshot
|
||||
.blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
|
||||
.enumerate()
|
||||
.filter_map(|(ix, (row, block))| {
|
||||
.filter_map(|(row, block)| {
|
||||
let block_id = block.id();
|
||||
let name: SharedString = match block {
|
||||
TransformBlock::Custom(block) => {
|
||||
Block::Custom(block) => {
|
||||
let mut element = block.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x: px(0.),
|
||||
@@ -984,7 +984,7 @@ fn editor_blocks(
|
||||
line_height: px(0.),
|
||||
em_width: px(0.),
|
||||
max_width: px(0.),
|
||||
block_id: ix,
|
||||
block_id,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
});
|
||||
let element = element.downcast_mut::<Stateful<Div>>().unwrap();
|
||||
@@ -996,7 +996,7 @@ fn editor_blocks(
|
||||
.ok()?
|
||||
}
|
||||
|
||||
TransformBlock::ExcerptHeader {
|
||||
Block::ExcerptHeader {
|
||||
starts_new_buffer, ..
|
||||
} => {
|
||||
if *starts_new_buffer {
|
||||
@@ -1005,7 +1005,7 @@ fn editor_blocks(
|
||||
EXCERPT_HEADER.into()
|
||||
}
|
||||
}
|
||||
TransformBlock::ExcerptFooter { .. } => EXCERPT_FOOTER.into(),
|
||||
Block::ExcerptFooter { .. } => EXCERPT_FOOTER.into(),
|
||||
};
|
||||
|
||||
Some((row, name))
|
||||
|
||||
1405
crates/diagnostics/src/grouped_diagnostics.rs
Normal file
1405
crates/diagnostics/src/grouped_diagnostics.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,12 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use crate::{grouped_diagnostics::GroupedDiagnosticsEditor, ProjectDiagnosticsEditor};
|
||||
use futures::future::Either;
|
||||
use gpui::{EventEmitter, ParentElement, Render, View, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{IconButton, IconName, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
pub struct ToolbarControls {
|
||||
editor: Option<WeakView<ProjectDiagnosticsEditor>>,
|
||||
editor: Option<Either<WeakView<ProjectDiagnosticsEditor>, WeakView<GroupedDiagnosticsEditor>>>,
|
||||
}
|
||||
|
||||
impl Render for ToolbarControls {
|
||||
@@ -14,18 +15,33 @@ impl Render for ToolbarControls {
|
||||
let mut has_stale_excerpts = false;
|
||||
let mut is_updating = false;
|
||||
|
||||
if let Some(editor) = self.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
let editor = editor.read(cx);
|
||||
|
||||
include_warnings = editor.include_warnings;
|
||||
has_stale_excerpts = !editor.paths_to_update.is_empty();
|
||||
is_updating = editor.update_paths_tx.len() > 0
|
||||
|| editor
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.next()
|
||||
.is_some();
|
||||
if let Some(editor) = self.editor() {
|
||||
match editor {
|
||||
Either::Left(editor) => {
|
||||
let editor = editor.read(cx);
|
||||
include_warnings = editor.include_warnings;
|
||||
has_stale_excerpts = !editor.paths_to_update.is_empty();
|
||||
is_updating = editor.update_paths_tx.len() > 0
|
||||
|| editor
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.next()
|
||||
.is_some();
|
||||
}
|
||||
Either::Right(editor) => {
|
||||
let editor = editor.read(cx);
|
||||
include_warnings = editor.include_warnings;
|
||||
has_stale_excerpts = !editor.paths_to_update.is_empty();
|
||||
is_updating = editor.update_paths_tx.len() > 0
|
||||
|| editor
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.next()
|
||||
.is_some();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
@@ -42,12 +58,19 @@ impl Render for ToolbarControls {
|
||||
.disabled(is_updating)
|
||||
.tooltip(move |cx| Tooltip::text("Update excerpts", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) =
|
||||
this.editor.as_ref().and_then(|editor| editor.upgrade())
|
||||
{
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.enqueue_update_stale_excerpts(None);
|
||||
});
|
||||
if let Some(editor) = this.editor() {
|
||||
match editor {
|
||||
Either::Left(editor) => {
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.enqueue_update_stale_excerpts(None);
|
||||
});
|
||||
}
|
||||
Either::Right(editor) => {
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.enqueue_update_stale_excerpts(None);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
@@ -56,12 +79,19 @@ impl Render for ToolbarControls {
|
||||
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) =
|
||||
this.editor.as_ref().and_then(|editor| editor.upgrade())
|
||||
{
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
if let Some(editor) = this.editor() {
|
||||
match editor {
|
||||
Either::Left(editor) => {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
Either::Right(editor) => {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
@@ -78,7 +108,10 @@ impl ToolbarItemView for ToolbarControls {
|
||||
) -> ToolbarItemLocation {
|
||||
if let Some(pane_item) = active_pane_item.as_ref() {
|
||||
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
|
||||
self.editor = Some(editor.downgrade());
|
||||
self.editor = Some(Either::Left(editor.downgrade()));
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else if let Some(editor) = pane_item.downcast::<GroupedDiagnosticsEditor>() {
|
||||
self.editor = Some(Either::Right(editor.downgrade()));
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
@@ -93,4 +126,13 @@ impl ToolbarControls {
|
||||
pub fn new() -> Self {
|
||||
ToolbarControls { editor: None }
|
||||
}
|
||||
|
||||
fn editor(
|
||||
&self,
|
||||
) -> Option<Either<View<ProjectDiagnosticsEditor>, View<GroupedDiagnosticsEditor>>> {
|
||||
Some(match self.editor.as_ref()? {
|
||||
Either::Left(diagnostics) => Either::Left(diagnostics.upgrade()?),
|
||||
Either::Right(grouped_diagnostics) => Either::Right(grouped_diagnostics.upgrade()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,14 @@ test-support = [
|
||||
aho-corasick = "1.1"
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
convert_case = "0.6.0"
|
||||
db.workspace = true
|
||||
emojis.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
@@ -49,6 +51,7 @@ lazy_static.workspace = true
|
||||
linkify.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
@@ -129,7 +129,7 @@ pub struct ExpandExcerptsDown {
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct ShowCompletions {
|
||||
#[serde(default)]
|
||||
pub(super) trigger: Option<char>,
|
||||
pub(super) trigger: Option<String>,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
@@ -286,12 +286,14 @@ gpui::actions!(
|
||||
SelectPageUp,
|
||||
ShowCharacterPalette,
|
||||
ShowInlineCompletion,
|
||||
ShowSignatureHelp,
|
||||
ShuffleLines,
|
||||
SortLinesCaseInsensitive,
|
||||
SortLinesCaseSensitive,
|
||||
SplitSelectionIntoLines,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
ToggleSelectionMenu,
|
||||
|
||||
@@ -8,6 +8,7 @@ use gpui::{
|
||||
use settings::Settings;
|
||||
use std::hash::Hash;
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use time::UtcOffset;
|
||||
use ui::{
|
||||
div, h_flex, tooltip_container, v_flex, Avatar, Button, ButtonStyle, Clickable as _, Color,
|
||||
FluentBuilder, Icon, IconName, IconPosition, InteractiveElement as _, IntoElement,
|
||||
@@ -129,7 +130,7 @@ impl Render for BlameEntryTooltip {
|
||||
let author_email = self.blame_entry.author_mail.clone();
|
||||
|
||||
let short_commit_id = self.blame_entry.sha.display_short();
|
||||
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry, cx);
|
||||
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry);
|
||||
|
||||
let message = self
|
||||
.details
|
||||
@@ -247,30 +248,25 @@ impl Render for BlameEntryTooltip {
|
||||
}
|
||||
}
|
||||
|
||||
fn blame_entry_timestamp(
|
||||
blame_entry: &BlameEntry,
|
||||
format: time_format::TimestampFormat,
|
||||
cx: &WindowContext,
|
||||
) -> String {
|
||||
fn blame_entry_timestamp(blame_entry: &BlameEntry, format: time_format::TimestampFormat) -> String {
|
||||
match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
cx.local_timezone(),
|
||||
format,
|
||||
),
|
||||
Ok(timestamp) => {
|
||||
let local = chrono::Local::now().offset().local_minus_utc();
|
||||
time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
UtcOffset::from_whole_seconds(local).unwrap(),
|
||||
format,
|
||||
)
|
||||
}
|
||||
Err(_) => "Error parsing date".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative, cx)
|
||||
pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative)
|
||||
}
|
||||
|
||||
fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
|
||||
blame_entry_timestamp(
|
||||
blame_entry,
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
cx,
|
||||
)
|
||||
fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::MediumAbsolute)
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ use crate::{
|
||||
hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt,
|
||||
};
|
||||
pub use block_map::{
|
||||
BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
|
||||
BlockMap, BlockPoint, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
|
||||
BlockMap, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
};
|
||||
use block_map::{BlockRow, BlockSnapshot};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -269,7 +269,7 @@ impl DisplayMap {
|
||||
&mut self,
|
||||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Vec<BlockId> {
|
||||
) -> Vec<CustomBlockId> {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
@@ -285,7 +285,7 @@ impl DisplayMap {
|
||||
|
||||
pub fn replace_blocks(
|
||||
&mut self,
|
||||
heights_and_renderers: HashMap<BlockId, (Option<u8>, RenderBlock)>,
|
||||
heights_and_renderers: HashMap<CustomBlockId, (Option<u8>, RenderBlock)>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
//
|
||||
@@ -306,8 +306,8 @@ impl DisplayMap {
|
||||
// directly and the new behavior separately.
|
||||
//
|
||||
//
|
||||
let mut only_renderers = HashMap::<BlockId, RenderBlock>::default();
|
||||
let mut full_replace = HashMap::<BlockId, (u8, RenderBlock)>::default();
|
||||
let mut only_renderers = HashMap::<CustomBlockId, RenderBlock>::default();
|
||||
let mut full_replace = HashMap::<CustomBlockId, (u8, RenderBlock)>::default();
|
||||
for (id, (height, render)) in heights_and_renderers {
|
||||
if let Some(height) = height {
|
||||
full_replace.insert(id, (height, render));
|
||||
@@ -334,7 +334,7 @@ impl DisplayMap {
|
||||
block_map.replace(full_replace);
|
||||
}
|
||||
|
||||
pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
|
||||
pub fn remove_blocks(&mut self, ids: HashSet<CustomBlockId>, cx: &mut ModelContext<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
@@ -350,7 +350,7 @@ impl DisplayMap {
|
||||
|
||||
pub fn row_for_block(
|
||||
&mut self,
|
||||
block_id: BlockId,
|
||||
block_id: CustomBlockId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<DisplayRow> {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
@@ -885,12 +885,16 @@ impl DisplaySnapshot {
|
||||
pub fn blocks_in_range(
|
||||
&self,
|
||||
rows: Range<DisplayRow>,
|
||||
) -> impl Iterator<Item = (DisplayRow, &TransformBlock)> {
|
||||
) -> impl Iterator<Item = (DisplayRow, &Block)> {
|
||||
self.block_snapshot
|
||||
.blocks_in_range(rows.start.0..rows.end.0)
|
||||
.map(|(row, block)| (DisplayRow(row), block))
|
||||
}
|
||||
|
||||
pub fn block_for_id(&self, id: BlockId) -> Option<Block> {
|
||||
self.block_snapshot.block_for_id(id)
|
||||
}
|
||||
|
||||
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
|
||||
self.fold_snapshot.intersects_fold(offset)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::{
|
||||
};
|
||||
use crate::{EditorStyle, GutterDimensions};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{AnyElement, Pixels, WindowContext};
|
||||
use gpui::{AnyElement, EntityId, Pixels, WindowContext};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, MultiBufferRow, ToPoint as _};
|
||||
use parking_lot::Mutex;
|
||||
@@ -18,8 +18,9 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use sum_tree::{Bias, SumTree, TreeMap};
|
||||
use text::Edit;
|
||||
use ui::ElementId;
|
||||
|
||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||
|
||||
@@ -29,7 +30,8 @@ const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||
pub struct BlockMap {
|
||||
next_block_id: AtomicUsize,
|
||||
wrap_snapshot: RefCell<WrapSnapshot>,
|
||||
blocks: Vec<Arc<Block>>,
|
||||
custom_blocks: Vec<Arc<CustomBlock>>,
|
||||
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
|
||||
transforms: RefCell<SumTree<Transform>>,
|
||||
show_excerpt_controls: bool,
|
||||
buffer_header_height: u8,
|
||||
@@ -38,7 +40,7 @@ pub struct BlockMap {
|
||||
}
|
||||
|
||||
pub struct BlockMapReader<'a> {
|
||||
blocks: &'a Vec<Arc<Block>>,
|
||||
blocks: &'a Vec<Arc<CustomBlock>>,
|
||||
pub snapshot: BlockSnapshot,
|
||||
}
|
||||
|
||||
@@ -48,10 +50,17 @@ pub struct BlockMapWriter<'a>(&'a mut BlockMap);
|
||||
pub struct BlockSnapshot {
|
||||
wrap_snapshot: WrapSnapshot,
|
||||
transforms: SumTree<Transform>,
|
||||
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct BlockId(usize);
|
||||
pub struct CustomBlockId(usize);
|
||||
|
||||
impl Into<ElementId> for CustomBlockId {
|
||||
fn into(self) -> ElementId {
|
||||
ElementId::Integer(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct BlockPoint(pub Point);
|
||||
@@ -62,10 +71,10 @@ pub struct BlockRow(pub(super) u32);
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
struct WrapRow(u32);
|
||||
|
||||
pub type RenderBlock = Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>;
|
||||
pub type RenderBlock = Box<dyn Send + FnMut(&mut BlockContext) -> AnyElement>;
|
||||
|
||||
pub struct Block {
|
||||
id: BlockId,
|
||||
pub struct CustomBlock {
|
||||
id: CustomBlockId,
|
||||
position: Anchor,
|
||||
height: u8,
|
||||
style: BlockStyle,
|
||||
@@ -77,11 +86,22 @@ pub struct BlockProperties<P> {
|
||||
pub position: P,
|
||||
pub height: u8,
|
||||
pub style: BlockStyle,
|
||||
pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,
|
||||
pub render: RenderBlock,
|
||||
pub disposition: BlockDisposition,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
impl<P: Debug> Debug for BlockProperties<P> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BlockProperties")
|
||||
.field("position", &self.position)
|
||||
.field("height", &self.height)
|
||||
.field("style", &self.style)
|
||||
.field("disposition", &self.disposition)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum BlockStyle {
|
||||
Fixed,
|
||||
Flex,
|
||||
@@ -95,10 +115,47 @@ pub struct BlockContext<'a, 'b> {
|
||||
pub gutter_dimensions: &'b GutterDimensions,
|
||||
pub em_width: Pixels,
|
||||
pub line_height: Pixels,
|
||||
pub block_id: usize,
|
||||
pub block_id: BlockId,
|
||||
pub editor_style: &'b EditorStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum BlockId {
|
||||
Custom(CustomBlockId),
|
||||
ExcerptHeader(ExcerptId),
|
||||
ExcerptFooter(ExcerptId),
|
||||
}
|
||||
|
||||
impl From<BlockId> for EntityId {
|
||||
fn from(value: BlockId) -> Self {
|
||||
match value {
|
||||
BlockId::Custom(CustomBlockId(id)) => EntityId::from(id as u64),
|
||||
BlockId::ExcerptHeader(id) => id.into(),
|
||||
BlockId::ExcerptFooter(id) => id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<ElementId> for BlockId {
|
||||
fn into(self) -> ElementId {
|
||||
match self {
|
||||
Self::Custom(CustomBlockId(id)) => ("Block", id).into(),
|
||||
Self::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(),
|
||||
Self::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BlockId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Custom(id) => write!(f, "Block({id:?})"),
|
||||
Self::ExcerptHeader(id) => write!(f, "ExcerptHeader({id:?})"),
|
||||
Self::ExcerptFooter(id) => write!(f, "ExcerptFooter({id:?})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the block should be considered above or below the anchor line
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum BlockDisposition {
|
||||
@@ -109,11 +166,11 @@ pub enum BlockDisposition {
|
||||
#[derive(Clone, Debug)]
|
||||
struct Transform {
|
||||
summary: TransformSummary,
|
||||
block: Option<TransformBlock>,
|
||||
block: Option<Block>,
|
||||
}
|
||||
|
||||
pub(crate) enum BlockType {
|
||||
Custom(BlockId),
|
||||
Custom(CustomBlockId),
|
||||
Header,
|
||||
Footer,
|
||||
}
|
||||
@@ -125,8 +182,8 @@ pub(crate) trait BlockLike {
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone)]
|
||||
pub enum TransformBlock {
|
||||
Custom(Arc<Block>),
|
||||
pub enum Block {
|
||||
Custom(Arc<CustomBlock>),
|
||||
ExcerptHeader {
|
||||
id: ExcerptId,
|
||||
buffer: BufferSnapshot,
|
||||
@@ -142,12 +199,12 @@ pub enum TransformBlock {
|
||||
},
|
||||
}
|
||||
|
||||
impl BlockLike for TransformBlock {
|
||||
impl BlockLike for Block {
|
||||
fn block_type(&self) -> BlockType {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => BlockType::Custom(block.id),
|
||||
TransformBlock::ExcerptHeader { .. } => BlockType::Header,
|
||||
TransformBlock::ExcerptFooter { .. } => BlockType::Footer,
|
||||
Block::Custom(block) => BlockType::Custom(block.id),
|
||||
Block::ExcerptHeader { .. } => BlockType::Header,
|
||||
Block::ExcerptFooter { .. } => BlockType::Footer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,25 +213,41 @@ impl BlockLike for TransformBlock {
|
||||
}
|
||||
}
|
||||
|
||||
impl TransformBlock {
|
||||
impl Block {
|
||||
pub fn id(&self) -> BlockId {
|
||||
match self {
|
||||
Block::Custom(block) => BlockId::Custom(block.id),
|
||||
Block::ExcerptHeader { id, .. } => BlockId::ExcerptHeader(*id),
|
||||
Block::ExcerptFooter { id, .. } => BlockId::ExcerptFooter(*id),
|
||||
}
|
||||
}
|
||||
|
||||
fn disposition(&self) -> BlockDisposition {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => block.disposition,
|
||||
TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above,
|
||||
TransformBlock::ExcerptFooter { disposition, .. } => *disposition,
|
||||
Block::Custom(block) => block.disposition,
|
||||
Block::ExcerptHeader { .. } => BlockDisposition::Above,
|
||||
Block::ExcerptFooter { disposition, .. } => *disposition,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u8 {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => block.height,
|
||||
TransformBlock::ExcerptHeader { height, .. } => *height,
|
||||
TransformBlock::ExcerptFooter { height, .. } => *height,
|
||||
Block::Custom(block) => block.height,
|
||||
Block::ExcerptHeader { height, .. } => *height,
|
||||
Block::ExcerptFooter { height, .. } => *height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self) -> BlockStyle {
|
||||
match self {
|
||||
Block::Custom(block) => block.style,
|
||||
Block::ExcerptHeader { .. } => BlockStyle::Sticky,
|
||||
Block::ExcerptFooter { .. } => BlockStyle::Sticky,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TransformBlock {
|
||||
impl Debug for Block {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
|
||||
@@ -189,7 +262,7 @@ impl Debug for TransformBlock {
|
||||
.field("path", &buffer.file().map(|f| f.path()))
|
||||
.field("starts_new_buffer", &starts_new_buffer)
|
||||
.finish(),
|
||||
TransformBlock::ExcerptFooter {
|
||||
Block::ExcerptFooter {
|
||||
id, disposition, ..
|
||||
} => f
|
||||
.debug_struct("ExcerptFooter")
|
||||
@@ -233,7 +306,8 @@ impl BlockMap {
|
||||
let row_count = wrap_snapshot.max_point().row() + 1;
|
||||
let map = Self {
|
||||
next_block_id: AtomicUsize::new(0),
|
||||
blocks: Vec::new(),
|
||||
custom_blocks: Vec::new(),
|
||||
custom_blocks_by_id: TreeMap::default(),
|
||||
transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())),
|
||||
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
|
||||
show_excerpt_controls,
|
||||
@@ -255,10 +329,11 @@ impl BlockMap {
|
||||
self.sync(&wrap_snapshot, edits);
|
||||
*self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
|
||||
BlockMapReader {
|
||||
blocks: &self.blocks,
|
||||
blocks: &self.custom_blocks,
|
||||
snapshot: BlockSnapshot {
|
||||
wrap_snapshot,
|
||||
transforms: self.transforms.borrow().clone(),
|
||||
custom_blocks_by_id: self.custom_blocks_by_id.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -380,25 +455,26 @@ impl BlockMap {
|
||||
let new_buffer_start =
|
||||
wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left);
|
||||
let start_bound = Bound::Included(new_buffer_start);
|
||||
let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| {
|
||||
probe
|
||||
.position
|
||||
.to_point(buffer)
|
||||
.cmp(&new_buffer_start)
|
||||
.then(Ordering::Greater)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => last_block_ix + ix,
|
||||
};
|
||||
let start_block_ix =
|
||||
match self.custom_blocks[last_block_ix..].binary_search_by(|probe| {
|
||||
probe
|
||||
.position
|
||||
.to_point(buffer)
|
||||
.cmp(&new_buffer_start)
|
||||
.then(Ordering::Greater)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => last_block_ix + ix,
|
||||
};
|
||||
|
||||
let end_bound;
|
||||
let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() {
|
||||
end_bound = Bound::Unbounded;
|
||||
self.blocks.len()
|
||||
self.custom_blocks.len()
|
||||
} else {
|
||||
let new_buffer_end =
|
||||
wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left);
|
||||
end_bound = Bound::Excluded(new_buffer_end);
|
||||
match self.blocks[start_block_ix..].binary_search_by(|probe| {
|
||||
match self.custom_blocks[start_block_ix..].binary_search_by(|probe| {
|
||||
probe
|
||||
.position
|
||||
.to_point(buffer)
|
||||
@@ -411,24 +487,22 @@ impl BlockMap {
|
||||
last_block_ix = end_block_ix;
|
||||
|
||||
debug_assert!(blocks_in_edit.is_empty());
|
||||
blocks_in_edit.extend(
|
||||
self.blocks[start_block_ix..end_block_ix]
|
||||
.iter()
|
||||
.map(|block| {
|
||||
let mut position = block.position.to_point(buffer);
|
||||
match block.disposition {
|
||||
BlockDisposition::Above => position.column = 0,
|
||||
BlockDisposition::Below => {
|
||||
position.column = buffer.line_len(MultiBufferRow(position.row))
|
||||
}
|
||||
blocks_in_edit.extend(self.custom_blocks[start_block_ix..end_block_ix].iter().map(
|
||||
|block| {
|
||||
let mut position = block.position.to_point(buffer);
|
||||
match block.disposition {
|
||||
BlockDisposition::Above => position.column = 0,
|
||||
BlockDisposition::Below => {
|
||||
position.column = buffer.line_len(MultiBufferRow(position.row))
|
||||
}
|
||||
let position = wrap_snapshot.make_wrap_point(position, Bias::Left);
|
||||
(position.row(), TransformBlock::Custom(block.clone()))
|
||||
}),
|
||||
);
|
||||
}
|
||||
let position = wrap_snapshot.make_wrap_point(position, Bias::Left);
|
||||
(position.row(), Block::Custom(block.clone()))
|
||||
},
|
||||
));
|
||||
|
||||
if buffer.show_headers() {
|
||||
blocks_in_edit.extend(BlockMap::header_blocks(
|
||||
blocks_in_edit.extend(BlockMap::header_and_footer_blocks(
|
||||
self.show_excerpt_controls,
|
||||
self.excerpt_footer_height,
|
||||
self.buffer_header_height,
|
||||
@@ -475,8 +549,8 @@ impl BlockMap {
|
||||
*transforms = new_transforms;
|
||||
}
|
||||
|
||||
pub fn replace_renderers(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
|
||||
for block in &mut self.blocks {
|
||||
pub fn replace_renderers(&mut self, mut renderers: HashMap<CustomBlockId, RenderBlock>) {
|
||||
for block in &mut self.custom_blocks {
|
||||
if let Some(render) = renderers.remove(&block.id) {
|
||||
*block.render.lock() = render;
|
||||
}
|
||||
@@ -487,7 +561,7 @@ impl BlockMap {
|
||||
self.show_excerpt_controls
|
||||
}
|
||||
|
||||
pub fn header_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>(
|
||||
pub fn header_and_footer_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>(
|
||||
show_excerpt_controls: bool,
|
||||
excerpt_footer_height: u8,
|
||||
buffer_header_height: u8,
|
||||
@@ -495,7 +569,7 @@ impl BlockMap {
|
||||
buffer: &'b multi_buffer::MultiBufferSnapshot,
|
||||
range: R,
|
||||
wrap_snapshot: &'c WrapSnapshot,
|
||||
) -> impl Iterator<Item = (u32, TransformBlock)> + 'b
|
||||
) -> impl Iterator<Item = (u32, Block)> + 'b
|
||||
where
|
||||
R: RangeBounds<T>,
|
||||
T: multi_buffer::ToOffset,
|
||||
@@ -503,24 +577,36 @@ impl BlockMap {
|
||||
buffer
|
||||
.excerpt_boundaries_in_range(range)
|
||||
.flat_map(move |excerpt_boundary| {
|
||||
let wrap_row = wrap_snapshot
|
||||
let mut wrap_row = wrap_snapshot
|
||||
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
|
||||
.row();
|
||||
|
||||
[
|
||||
show_excerpt_controls
|
||||
.then(|| {
|
||||
let disposition;
|
||||
if excerpt_boundary.next.is_some() {
|
||||
disposition = BlockDisposition::Above;
|
||||
} else {
|
||||
wrap_row = wrap_snapshot
|
||||
.make_wrap_point(
|
||||
Point::new(
|
||||
excerpt_boundary.row.0,
|
||||
buffer.line_len(excerpt_boundary.row),
|
||||
),
|
||||
Bias::Left,
|
||||
)
|
||||
.row();
|
||||
disposition = BlockDisposition::Below;
|
||||
}
|
||||
|
||||
excerpt_boundary.prev.as_ref().map(|prev| {
|
||||
(
|
||||
wrap_row,
|
||||
TransformBlock::ExcerptFooter {
|
||||
Block::ExcerptFooter {
|
||||
id: prev.id,
|
||||
height: excerpt_footer_height,
|
||||
disposition: if excerpt_boundary.next.is_some() {
|
||||
BlockDisposition::Above
|
||||
} else {
|
||||
BlockDisposition::Below
|
||||
},
|
||||
disposition,
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -533,7 +619,7 @@ impl BlockMap {
|
||||
|
||||
(
|
||||
wrap_row,
|
||||
TransformBlock::ExcerptHeader {
|
||||
Block::ExcerptHeader {
|
||||
id: next.id,
|
||||
buffer: next.buffer,
|
||||
range: next.range,
|
||||
@@ -629,7 +715,7 @@ impl<'a> DerefMut for BlockMapReader<'a> {
|
||||
}
|
||||
|
||||
impl<'a> BlockMapReader<'a> {
|
||||
pub fn row_for_block(&self, block_id: BlockId) -> Option<BlockRow> {
|
||||
pub fn row_for_block(&self, block_id: CustomBlockId) -> Option<BlockRow> {
|
||||
let block = self.blocks.iter().find(|block| block.id == block_id)?;
|
||||
let buffer_row = block
|
||||
.position
|
||||
@@ -674,14 +760,14 @@ impl<'a> BlockMapWriter<'a> {
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
) -> Vec<BlockId> {
|
||||
) -> Vec<CustomBlockId> {
|
||||
let mut ids = Vec::new();
|
||||
let mut edits = Patch::default();
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
|
||||
for block in blocks {
|
||||
let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst));
|
||||
let id = CustomBlockId(self.0.next_block_id.fetch_add(1, SeqCst));
|
||||
ids.push(id);
|
||||
|
||||
let position = block.position;
|
||||
@@ -696,22 +782,21 @@ impl<'a> BlockMapWriter<'a> {
|
||||
|
||||
let block_ix = match self
|
||||
.0
|
||||
.blocks
|
||||
.custom_blocks
|
||||
.binary_search_by(|probe| probe.position.cmp(&position, buffer))
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
self.0.blocks.insert(
|
||||
block_ix,
|
||||
Arc::new(Block {
|
||||
id,
|
||||
position,
|
||||
height: block.height,
|
||||
render: Mutex::new(block.render),
|
||||
disposition: block.disposition,
|
||||
style: block.style,
|
||||
}),
|
||||
);
|
||||
let new_block = Arc::new(CustomBlock {
|
||||
id,
|
||||
position,
|
||||
height: block.height,
|
||||
render: Mutex::new(block.render),
|
||||
disposition: block.disposition,
|
||||
style: block.style,
|
||||
});
|
||||
self.0.custom_blocks.insert(block_ix, new_block.clone());
|
||||
self.0.custom_blocks_by_id.insert(id, new_block);
|
||||
|
||||
edits = edits.compose([Edit {
|
||||
old: start_row..end_row,
|
||||
@@ -723,16 +808,19 @@ impl<'a> BlockMapWriter<'a> {
|
||||
ids
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, mut heights_and_renderers: HashMap<BlockId, (u8, RenderBlock)>) {
|
||||
pub fn replace(
|
||||
&mut self,
|
||||
mut heights_and_renderers: HashMap<CustomBlockId, (u8, RenderBlock)>,
|
||||
) {
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
let mut edits = Patch::default();
|
||||
let mut last_block_buffer_row = None;
|
||||
|
||||
for block in &mut self.0.blocks {
|
||||
for block in &mut self.0.custom_blocks {
|
||||
if let Some((new_height, render)) = heights_and_renderers.remove(&block.id) {
|
||||
if block.height != new_height {
|
||||
let new_block = Block {
|
||||
let new_block = CustomBlock {
|
||||
id: block.id,
|
||||
position: block.position,
|
||||
height: new_height,
|
||||
@@ -740,7 +828,9 @@ impl<'a> BlockMapWriter<'a> {
|
||||
render: Mutex::new(render),
|
||||
disposition: block.disposition,
|
||||
};
|
||||
*block = Arc::new(new_block);
|
||||
let new_block = Arc::new(new_block);
|
||||
*block = new_block.clone();
|
||||
self.0.custom_blocks_by_id.insert(block.id, new_block);
|
||||
|
||||
let buffer_row = block.position.to_point(buffer).row;
|
||||
if last_block_buffer_row != Some(buffer_row) {
|
||||
@@ -765,12 +855,12 @@ impl<'a> BlockMapWriter<'a> {
|
||||
self.0.sync(wrap_snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, block_ids: HashSet<BlockId>) {
|
||||
pub fn remove(&mut self, block_ids: HashSet<CustomBlockId>) {
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
let mut edits = Patch::default();
|
||||
let mut last_block_buffer_row = None;
|
||||
self.0.blocks.retain(|block| {
|
||||
self.0.custom_blocks.retain(|block| {
|
||||
if block_ids.contains(&block.id) {
|
||||
let buffer_row = block.position.to_point(buffer).row;
|
||||
if last_block_buffer_row != Some(buffer_row) {
|
||||
@@ -787,6 +877,7 @@ impl<'a> BlockMapWriter<'a> {
|
||||
new: start_row..end_row,
|
||||
})
|
||||
}
|
||||
self.0.custom_blocks_by_id.remove(&block.id);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
@@ -871,10 +962,7 @@ impl BlockSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blocks_in_range(
|
||||
&self,
|
||||
rows: Range<u32>,
|
||||
) -> impl Iterator<Item = (u32, &TransformBlock)> {
|
||||
pub fn blocks_in_range(&self, rows: Range<u32>) -> impl Iterator<Item = (u32, &Block)> {
|
||||
let mut cursor = self.transforms.cursor::<BlockRow>();
|
||||
cursor.seek(&BlockRow(rows.start), Bias::Right, &());
|
||||
std::iter::from_fn(move || {
|
||||
@@ -894,6 +982,60 @@ impl BlockSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn block_for_id(&self, block_id: BlockId) -> Option<Block> {
|
||||
let buffer = self.wrap_snapshot.buffer_snapshot();
|
||||
|
||||
match block_id {
|
||||
BlockId::Custom(custom_block_id) => {
|
||||
let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?;
|
||||
Some(Block::Custom(custom_block.clone()))
|
||||
}
|
||||
BlockId::ExcerptHeader(excerpt_id) => {
|
||||
let excerpt_range = buffer.range_for_excerpt::<Point>(excerpt_id)?;
|
||||
let wrap_point = self
|
||||
.wrap_snapshot
|
||||
.make_wrap_point(excerpt_range.start, Bias::Left);
|
||||
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>();
|
||||
cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &());
|
||||
while let Some(transform) = cursor.item() {
|
||||
if let Some(block) = transform.block.as_ref() {
|
||||
if block.id() == block_id {
|
||||
return Some(block.clone());
|
||||
}
|
||||
} else if cursor.start().0 > WrapRow(wrap_point.row()) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
BlockId::ExcerptFooter(excerpt_id) => {
|
||||
let excerpt_range = buffer.range_for_excerpt::<Point>(excerpt_id)?;
|
||||
let wrap_point = self
|
||||
.wrap_snapshot
|
||||
.make_wrap_point(excerpt_range.end, Bias::Left);
|
||||
|
||||
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>();
|
||||
cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &());
|
||||
while let Some(transform) = cursor.item() {
|
||||
if let Some(block) = transform.block.as_ref() {
|
||||
if block.id() == block_id {
|
||||
return Some(block.clone());
|
||||
}
|
||||
} else if cursor.start().0 > WrapRow(wrap_point.row()) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> BlockPoint {
|
||||
let row = self.transforms.summary().output_rows - 1;
|
||||
BlockPoint::new(row, self.line_len(BlockRow(row)))
|
||||
@@ -1023,7 +1165,7 @@ impl Transform {
|
||||
}
|
||||
}
|
||||
|
||||
fn block(block: TransformBlock) -> Self {
|
||||
fn block(block: Block) -> Self {
|
||||
Self {
|
||||
summary: TransformSummary {
|
||||
input_rows: 0,
|
||||
@@ -1172,7 +1314,7 @@ impl DerefMut for BlockContext<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Block {
|
||||
impl CustomBlock {
|
||||
pub fn render(&self, cx: &mut BlockContext) -> AnyElement {
|
||||
self.render.lock()(cx)
|
||||
}
|
||||
@@ -1186,7 +1328,7 @@ impl Block {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Block {
|
||||
impl Debug for CustomBlock {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Block")
|
||||
.field("id", &self.id)
|
||||
@@ -1216,15 +1358,16 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use super::*;
|
||||
use crate::display_map::inlay_map::InlayMap;
|
||||
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
|
||||
use gpui::{div, font, px, Element};
|
||||
use crate::display_map::{
|
||||
fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap,
|
||||
};
|
||||
use gpui::{div, font, px, AppContext, Context as _, Element};
|
||||
use language::{Buffer, Capability};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::env;
|
||||
use util::RandomCharIter;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1411,6 +1554,89 @@ mod tests {
|
||||
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_multibuffer_headers_and_footers(cx: &mut AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let buffer1 = cx.new_model(|cx| Buffer::local("Buffer 1", cx));
|
||||
let buffer2 = cx.new_model(|cx| Buffer::local("Buffer 2", cx));
|
||||
let buffer3 = cx.new_model(|cx| Buffer::local("Buffer 3", cx));
|
||||
|
||||
let mut excerpt_ids = Vec::new();
|
||||
let multi_buffer = cx.new_model(|cx| {
|
||||
let mut multi_buffer = MultiBuffer::new(0, Capability::ReadWrite);
|
||||
excerpt_ids.extend(multi_buffer.push_excerpts(
|
||||
buffer1.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer1.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
));
|
||||
excerpt_ids.extend(multi_buffer.push_excerpts(
|
||||
buffer2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer2.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
));
|
||||
excerpt_ids.extend(multi_buffer.push_excerpts(
|
||||
buffer3.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer3.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
));
|
||||
|
||||
multi_buffer
|
||||
});
|
||||
|
||||
let font = font("Helvetica");
|
||||
let font_size = px(14.);
|
||||
let font_id = cx.text_system().resolve_font(&font);
|
||||
let mut wrap_width = px(0.);
|
||||
for c in "Buff".chars() {
|
||||
wrap_width += cx
|
||||
.text_system()
|
||||
.advance(font_id, font_size, c)
|
||||
.unwrap()
|
||||
.width;
|
||||
}
|
||||
|
||||
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
|
||||
let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
|
||||
|
||||
let block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
|
||||
let snapshot = block_map.read(wraps_snapshot, Default::default());
|
||||
|
||||
// Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline.
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
"\nBuff\ner 1\n\n\nBuff\ner 2\n\n\nBuff\ner 3\n"
|
||||
);
|
||||
|
||||
let blocks: Vec<_> = snapshot
|
||||
.blocks_in_range(0..u32::MAX)
|
||||
.map(|(row, block)| (row, block.id()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
blocks,
|
||||
vec![
|
||||
(0, BlockId::ExcerptHeader(excerpt_ids[0])),
|
||||
(3, BlockId::ExcerptFooter(excerpt_ids[0])),
|
||||
(4, BlockId::ExcerptHeader(excerpt_ids[1])),
|
||||
(7, BlockId::ExcerptFooter(excerpt_ids[1])),
|
||||
(8, BlockId::ExcerptHeader(excerpt_ids[2])),
|
||||
(11, BlockId::ExcerptFooter(excerpt_ids[2]))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_with_heights(cx: &mut gpui::TestAppContext) {
|
||||
let _update = cx.update(|cx| init_test(cx));
|
||||
@@ -1744,7 +1970,7 @@ mod tests {
|
||||
|
||||
// Note that this needs to be synced with the related section in BlockMap::sync
|
||||
expected_blocks.extend(
|
||||
BlockMap::header_blocks(
|
||||
BlockMap::header_and_footer_blocks(
|
||||
true,
|
||||
excerpt_footer_height,
|
||||
buffer_start_header_height,
|
||||
@@ -1848,6 +2074,16 @@ mod tests {
|
||||
expected_block_positions
|
||||
);
|
||||
|
||||
for (_, expected_block) in
|
||||
blocks_snapshot.blocks_in_range(0..(expected_row_count as u32))
|
||||
{
|
||||
let actual_block = blocks_snapshot.block_for_id(expected_block.id());
|
||||
assert_eq!(
|
||||
actual_block.map(|block| block.id()),
|
||||
Some(expected_block.id())
|
||||
);
|
||||
}
|
||||
|
||||
for (block_row, block) in expected_block_positions {
|
||||
if let BlockType::Custom(block_id) = block.block_type() {
|
||||
assert_eq!(
|
||||
@@ -1944,7 +2180,7 @@ mod tests {
|
||||
},
|
||||
Custom {
|
||||
disposition: BlockDisposition,
|
||||
id: BlockId,
|
||||
id: CustomBlockId,
|
||||
height: u8,
|
||||
},
|
||||
}
|
||||
@@ -1981,15 +2217,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransformBlock> for ExpectedBlock {
|
||||
fn from(block: TransformBlock) -> Self {
|
||||
impl From<Block> for ExpectedBlock {
|
||||
fn from(block: Block) -> Self {
|
||||
match block {
|
||||
TransformBlock::Custom(block) => ExpectedBlock::Custom {
|
||||
Block::Custom(block) => ExpectedBlock::Custom {
|
||||
id: block.id,
|
||||
disposition: block.disposition,
|
||||
height: block.height,
|
||||
},
|
||||
TransformBlock::ExcerptHeader {
|
||||
Block::ExcerptHeader {
|
||||
height,
|
||||
starts_new_buffer,
|
||||
..
|
||||
@@ -1997,7 +2233,7 @@ mod tests {
|
||||
height,
|
||||
starts_new_buffer,
|
||||
},
|
||||
TransformBlock::ExcerptFooter {
|
||||
Block::ExcerptFooter {
|
||||
height,
|
||||
disposition,
|
||||
..
|
||||
@@ -2017,12 +2253,12 @@ mod tests {
|
||||
assets::Assets.load_test_fonts(cx);
|
||||
}
|
||||
|
||||
impl TransformBlock {
|
||||
fn as_custom(&self) -> Option<&Block> {
|
||||
impl Block {
|
||||
fn as_custom(&self) -> Option<&CustomBlock> {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => Some(block),
|
||||
TransformBlock::ExcerptHeader { .. } => None,
|
||||
TransformBlock::ExcerptFooter { .. } => None,
|
||||
Block::Custom(block) => Some(block),
|
||||
Block::ExcerptHeader { .. } => None,
|
||||
Block::ExcerptFooter { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
new_tree
|
||||
};
|
||||
|
||||
consolidate_inlay_edits(&mut edits);
|
||||
let edits = consolidate_inlay_edits(edits);
|
||||
let edits = self.0.sync(snapshot.clone(), edits);
|
||||
(self.0.snapshot.clone(), edits)
|
||||
}
|
||||
@@ -212,7 +212,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
folds
|
||||
};
|
||||
|
||||
consolidate_inlay_edits(&mut edits);
|
||||
let edits = consolidate_inlay_edits(edits);
|
||||
let edits = self.0.sync(snapshot.clone(), edits);
|
||||
(self.0.snapshot.clone(), edits)
|
||||
}
|
||||
@@ -513,7 +513,7 @@ impl FoldMap {
|
||||
});
|
||||
}
|
||||
|
||||
consolidate_fold_edits(&mut fold_edits);
|
||||
fold_edits = consolidate_fold_edits(fold_edits);
|
||||
}
|
||||
|
||||
self.snapshot.transforms = new_transforms;
|
||||
@@ -809,7 +809,7 @@ where
|
||||
cursor
|
||||
}
|
||||
|
||||
fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
|
||||
fn consolidate_inlay_edits(mut edits: Vec<InlayEdit>) -> Vec<InlayEdit> {
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
a.old
|
||||
.start
|
||||
@@ -817,42 +817,68 @@ fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
|
||||
.then_with(|| b.old.end.cmp(&a.old.end))
|
||||
});
|
||||
|
||||
let mut i = 1;
|
||||
while i < edits.len() {
|
||||
let edit = edits[i].clone();
|
||||
let prev_edit = &mut edits[i - 1];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
edits.remove(i);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let _old_alloc_ptr = edits.as_ptr();
|
||||
let mut inlay_edits = edits.into_iter();
|
||||
let inlay_edits = if let Some(mut first_edit) = inlay_edits.next() {
|
||||
// This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
|
||||
#[allow(clippy::filter_map_identity)]
|
||||
let mut v: Vec<_> = inlay_edits
|
||||
.scan(&mut first_edit, |prev_edit, edit| {
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
Some(None) // Skip this edit, it's merged
|
||||
} else {
|
||||
let prev = std::mem::replace(*prev_edit, edit);
|
||||
Some(Some(prev)) // Yield the previous edit
|
||||
}
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
v.push(first_edit.clone());
|
||||
debug_assert_eq!(_old_alloc_ptr, v.as_ptr(), "Inlay edits were reallocated");
|
||||
v
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
inlay_edits
|
||||
}
|
||||
|
||||
fn consolidate_fold_edits(edits: &mut Vec<FoldEdit>) {
|
||||
fn consolidate_fold_edits(mut edits: Vec<FoldEdit>) -> Vec<FoldEdit> {
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
a.old
|
||||
.start
|
||||
.cmp(&b.old.start)
|
||||
.then_with(|| b.old.end.cmp(&a.old.end))
|
||||
});
|
||||
let _old_alloc_ptr = edits.as_ptr();
|
||||
let mut fold_edits = edits.into_iter();
|
||||
let fold_edits = if let Some(mut first_edit) = fold_edits.next() {
|
||||
// This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
|
||||
#[allow(clippy::filter_map_identity)]
|
||||
let mut v: Vec<_> = fold_edits
|
||||
.scan(&mut first_edit, |prev_edit, edit| {
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
Some(None) // Skip this edit, it's merged
|
||||
} else {
|
||||
let prev = std::mem::replace(*prev_edit, edit);
|
||||
Some(Some(prev)) // Yield the previous edit
|
||||
}
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
v.push(first_edit.clone());
|
||||
v
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let mut i = 1;
|
||||
while i < edits.len() {
|
||||
let edit = edits[i].clone();
|
||||
let prev_edit = &mut edits[i - 1];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
edits.remove(i);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
fold_edits
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -103,20 +103,33 @@ impl TabMap {
|
||||
}
|
||||
}
|
||||
|
||||
let _old_alloc_ptr = fold_edits.as_ptr();
|
||||
// Combine any edits that overlap due to the expansion.
|
||||
let mut ix = 1;
|
||||
while ix < fold_edits.len() {
|
||||
let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
|
||||
let prev_edit = prev_edits.last_mut().unwrap();
|
||||
let edit = &next_edits[0];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = edit.old.end;
|
||||
prev_edit.new.end = edit.new.end;
|
||||
fold_edits.remove(ix);
|
||||
} else {
|
||||
ix += 1;
|
||||
}
|
||||
}
|
||||
let mut fold_edits = fold_edits.into_iter();
|
||||
let fold_edits = if let Some(mut first_edit) = fold_edits.next() {
|
||||
// This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
|
||||
#[allow(clippy::filter_map_identity)]
|
||||
let mut v: Vec<_> = fold_edits
|
||||
.scan(&mut first_edit, |state, edit| {
|
||||
if state.old.end >= edit.old.start {
|
||||
state.old.end = edit.old.end;
|
||||
state.new.end = edit.new.end;
|
||||
Some(None) // Skip this edit, it's merged
|
||||
} else {
|
||||
let new_state = edit.clone();
|
||||
let result = Some(Some(state.clone())); // Yield the previous edit
|
||||
**state = new_state;
|
||||
result
|
||||
}
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
v.push(first_edit);
|
||||
debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Fold edits were reallocated");
|
||||
v
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
for fold_edit in fold_edits {
|
||||
let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
|
||||
|
||||
@@ -567,7 +567,7 @@ impl WrapSnapshot {
|
||||
});
|
||||
}
|
||||
|
||||
consolidate_wrap_edits(&mut wrap_edits);
|
||||
wrap_edits = consolidate_wrap_edits(wrap_edits);
|
||||
Patch::new(wrap_edits)
|
||||
}
|
||||
|
||||
@@ -1008,19 +1008,33 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
|
||||
}
|
||||
}
|
||||
|
||||
fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
|
||||
let mut i = 1;
|
||||
while i < edits.len() {
|
||||
let edit = edits[i].clone();
|
||||
let prev_edit = &mut edits[i - 1];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = edit.old.end;
|
||||
prev_edit.new.end = edit.new.end;
|
||||
edits.remove(i);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
|
||||
let _old_alloc_ptr = edits.as_ptr();
|
||||
let mut wrap_edits = edits.into_iter();
|
||||
let wrap_edits = if let Some(mut first_edit) = wrap_edits.next() {
|
||||
// This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
|
||||
#[allow(clippy::filter_map_identity)]
|
||||
let mut v: Vec<_> = wrap_edits
|
||||
.scan(&mut first_edit, |prev_edit, edit| {
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = edit.old.end;
|
||||
prev_edit.new.end = edit.new.end;
|
||||
Some(None) // Skip this edit, it's merged
|
||||
} else {
|
||||
let prev = std::mem::replace(*prev_edit, edit);
|
||||
Some(Some(prev)) // Yield the previous edit
|
||||
}
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
v.push(first_edit.clone());
|
||||
debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Wrap edits were reallocated");
|
||||
v
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
wrap_edits
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -39,8 +39,10 @@ pub mod tasks;
|
||||
|
||||
#[cfg(test)]
|
||||
mod editor_tests;
|
||||
mod signature_help;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
use ::git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||
pub(crate) use actions::*;
|
||||
@@ -66,17 +68,17 @@ use git::diff_hunk_to_display;
|
||||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, FocusableView,
|
||||
FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
|
||||
Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle,
|
||||
UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle,
|
||||
WeakView, WhiteSpace, WindowContext,
|
||||
Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle, FocusOutEvent,
|
||||
FocusableView, FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText,
|
||||
KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render,
|
||||
SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
|
||||
UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
|
||||
WeakFocusHandle, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
use hunk_diff::ExpandedHunks;
|
||||
pub(crate) use hunk_diff::HunkToExpand;
|
||||
pub(crate) use hunk_diff::HoveredHunk;
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion_provider::*;
|
||||
@@ -129,7 +131,7 @@ use std::{
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -154,6 +156,7 @@ use workspace::{
|
||||
use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
|
||||
|
||||
use crate::hover_links::find_url;
|
||||
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
|
||||
|
||||
pub const FILE_HEADER_HEIGHT: u8 = 1;
|
||||
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u8 = 1;
|
||||
@@ -268,8 +271,9 @@ pub fn init(cx: &mut AppContext) {
|
||||
init_settings(cx);
|
||||
|
||||
workspace::register_project_item::<Editor>(cx);
|
||||
workspace::register_followable_item::<Editor>(cx);
|
||||
workspace::register_deserializable_item::<Editor>(cx);
|
||||
workspace::FollowableViewRegistry::register::<Editor>(cx);
|
||||
workspace::register_serializable_item::<Editor>(cx);
|
||||
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(Editor::new_file);
|
||||
@@ -501,6 +505,8 @@ pub struct Editor {
|
||||
context_menu: RwLock<Option<ContextMenu>>,
|
||||
mouse_context_menu: Option<MouseContextMenu>,
|
||||
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
||||
signature_help_state: SignatureHelpState,
|
||||
auto_signature_help: Option<bool>,
|
||||
find_all_references_task_sources: Vec<Anchor>,
|
||||
next_completion_id: CompletionId,
|
||||
completion_documentation_pre_resolve_debounce: DebouncedDelay,
|
||||
@@ -545,6 +551,7 @@ pub struct Editor {
|
||||
show_git_blame_inline: bool,
|
||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||
git_blame_inline_enabled: bool,
|
||||
serialize_dirty_buffers: bool,
|
||||
show_selection_menu: Option<bool>,
|
||||
blame: Option<Model<GitBlame>>,
|
||||
blame_subscription: Option<Subscription>,
|
||||
@@ -561,6 +568,7 @@ pub struct Editor {
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
file_header_size: u8,
|
||||
breadcrumb_header: Option<String>,
|
||||
focused_block: Option<FocusedBlock>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -778,7 +786,7 @@ pub struct RenameState {
|
||||
pub range: Range<Anchor>,
|
||||
pub old_name: Arc<str>,
|
||||
pub editor: View<Editor>,
|
||||
block_id: BlockId,
|
||||
block_id: CustomBlockId,
|
||||
}
|
||||
|
||||
struct InvalidationStack<T>(Vec<T>);
|
||||
@@ -1530,7 +1538,7 @@ struct ActiveDiagnosticGroup {
|
||||
primary_range: Range<Anchor>,
|
||||
primary_message: String,
|
||||
group_id: usize,
|
||||
blocks: HashMap<BlockId, Diagnostic>,
|
||||
blocks: HashMap<CustomBlockId, Diagnostic>,
|
||||
is_valid: bool,
|
||||
}
|
||||
|
||||
@@ -1578,6 +1586,11 @@ impl InlayHintRefreshReason {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FocusedBlock {
|
||||
id: BlockId,
|
||||
focus_handle: WeakFocusHandle,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn single_line(cx: &mut ViewContext<Self>) -> Self {
|
||||
let buffer = cx.new_model(|cx| Buffer::local("", cx));
|
||||
@@ -1765,6 +1778,8 @@ impl Editor {
|
||||
);
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, Self::handle_focus).detach();
|
||||
cx.on_focus_in(&focus_handle, Self::handle_focus_in)
|
||||
.detach();
|
||||
cx.on_focus_out(&focus_handle, Self::handle_focus_out)
|
||||
.detach();
|
||||
cx.on_blur(&focus_handle, Self::handle_blur).detach();
|
||||
@@ -1819,6 +1834,8 @@ impl Editor {
|
||||
context_menu: RwLock::new(None),
|
||||
mouse_context_menu: None,
|
||||
completion_tasks: Default::default(),
|
||||
signature_help_state: SignatureHelpState::default(),
|
||||
auto_signature_help: None,
|
||||
find_all_references_task_sources: Vec::new(),
|
||||
next_completion_id: 0,
|
||||
completion_documentation_pre_resolve_debounce: DebouncedDelay::new(),
|
||||
@@ -1867,6 +1884,9 @@ impl Editor {
|
||||
show_selection_menu: None,
|
||||
show_git_blame_inline_delay_task: None,
|
||||
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
||||
serialize_dirty_buffers: ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers,
|
||||
blame: None,
|
||||
blame_subscription: None,
|
||||
file_header_size,
|
||||
@@ -1894,6 +1914,7 @@ impl Editor {
|
||||
linked_edit_ranges: Default::default(),
|
||||
previous_search_ranges: None,
|
||||
breadcrumb_header: None,
|
||||
focused_block: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
@@ -1978,28 +1999,35 @@ impl Editor {
|
||||
_: &workspace::NewFile,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Self::new_in_workspace(workspace, cx).detach_and_prompt_err(
|
||||
"Failed to create buffer",
|
||||
cx,
|
||||
|e, _| match e.error_code() {
|
||||
ErrorCode::RemoteUpgradeRequired => Some(format!(
|
||||
"The remote instance of Zed does not support this yet. It must be upgraded to {}",
|
||||
e.error_tag("required").unwrap_or("the latest version")
|
||||
)),
|
||||
_ => None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn new_in_workspace(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Task<Result<View<Editor>>> {
|
||||
let project = workspace.project().clone();
|
||||
let create = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let buffer = create.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(
|
||||
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
|
||||
),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
let editor =
|
||||
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx));
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
|
||||
editor
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to create buffer", cx, |e, _| match e.error_code() {
|
||||
ErrorCode::RemoteUpgradeRequired => Some(format!(
|
||||
"The remote instance of Zed does not support this yet. It must be upgraded to {}",
|
||||
e.error_tag("required").unwrap_or("the latest version")
|
||||
)),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn new_file_in_direction(
|
||||
@@ -2156,6 +2184,10 @@ impl Editor {
|
||||
|
||||
pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
|
||||
self.cursor_shape = cursor_shape;
|
||||
|
||||
// Disrupt blink for immediate user feedback that the cursor shape has changed
|
||||
self.blink_manager.update(cx, BlinkManager::show_cursor);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -2411,6 +2443,15 @@ impl Editor {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
self.selections_did_change(true, &old_cursor_position, request_completions, cx);
|
||||
|
||||
if self.should_open_signature_help_automatically(
|
||||
&old_cursor_position,
|
||||
self.signature_help_state.backspace_pressed(),
|
||||
cx,
|
||||
) {
|
||||
self.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
self.signature_help_state.set_backspace_pressed(false);
|
||||
}
|
||||
|
||||
result
|
||||
@@ -2839,7 +2880,10 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.clear_expanded_diff_hunks(cx);
|
||||
if self.clear_clicked_diff_hunks(cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if self.dismiss_menus_and_popups(true, cx) {
|
||||
return;
|
||||
}
|
||||
@@ -2866,10 +2910,18 @@ impl Editor {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.hide_context_menu(cx).is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.mouse_context_menu.take().is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.discard_inline_completion(should_report_inline_completion_event, cx) {
|
||||
return true;
|
||||
}
|
||||
@@ -2942,7 +2994,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let mut brace_inserted = false;
|
||||
let mut bracket_inserted = false;
|
||||
let mut edits = Vec::new();
|
||||
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||
let mut new_selections = Vec::with_capacity(selections.len());
|
||||
@@ -3004,6 +3056,7 @@ impl Editor {
|
||||
),
|
||||
&bracket_pair.start[..prefix_len],
|
||||
));
|
||||
|
||||
if autoclose
|
||||
&& bracket_pair.close
|
||||
&& following_text_allows_autoclose
|
||||
@@ -3021,7 +3074,7 @@ impl Editor {
|
||||
selection.range(),
|
||||
format!("{}{}", text, bracket_pair.end).into(),
|
||||
));
|
||||
brace_inserted = true;
|
||||
bracket_inserted = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -3067,7 +3120,7 @@ impl Editor {
|
||||
selection.end..selection.end,
|
||||
bracket_pair.end.as_str().into(),
|
||||
));
|
||||
brace_inserted = true;
|
||||
bracket_inserted = true;
|
||||
new_selections.push((
|
||||
Selection {
|
||||
id: selection.id,
|
||||
@@ -3224,7 +3277,7 @@ impl Editor {
|
||||
s.select(new_selections)
|
||||
});
|
||||
|
||||
if !brace_inserted && EditorSettings::get_global(cx).use_on_type_format {
|
||||
if !bracket_inserted && EditorSettings::get_global(cx).use_on_type_format {
|
||||
if let Some(on_type_format_task) =
|
||||
this.trigger_on_type_formatting(text.to_string(), cx)
|
||||
{
|
||||
@@ -3232,6 +3285,14 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
let editor_settings = EditorSettings::get_global(cx);
|
||||
if bracket_inserted
|
||||
&& (editor_settings.auto_signature_help
|
||||
|| editor_settings.show_signature_help_after_edits)
|
||||
{
|
||||
this.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
|
||||
let trigger_in_words = !had_active_inline_completion;
|
||||
this.trigger_completion_on_input(&text, trigger_in_words, cx);
|
||||
linked_editing_ranges::refresh_linked_ranges(this, cx);
|
||||
@@ -3598,7 +3659,7 @@ impl Editor {
|
||||
if self.is_completion_trigger(text, trigger_in_words, cx) {
|
||||
self.show_completions(
|
||||
&ShowCompletions {
|
||||
trigger: text.chars().last(),
|
||||
trigger: Some(text.to_owned()).filter(|x| !x.is_empty()),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -4022,15 +4083,18 @@ impl Editor {
|
||||
Some(ContextMenu::Completions(_))
|
||||
)
|
||||
};
|
||||
let trigger_kind = match (options.trigger, is_followup_invoke) {
|
||||
let trigger_kind = match (&options.trigger, is_followup_invoke) {
|
||||
(_, true) => CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS,
|
||||
(Some(_), _) => CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||
(Some(trigger), _) if buffer.read(cx).completion_triggers().contains(&trigger) => {
|
||||
CompletionTriggerKind::TRIGGER_CHARACTER
|
||||
}
|
||||
|
||||
_ => CompletionTriggerKind::INVOKED,
|
||||
};
|
||||
let completion_context = CompletionContext {
|
||||
trigger_character: options.trigger.and_then(|c| {
|
||||
trigger_character: options.trigger.as_ref().and_then(|trigger| {
|
||||
if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
|
||||
Some(String::from(c))
|
||||
Some(String::from(trigger))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -4305,6 +4369,14 @@ impl Editor {
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
|
||||
let editor_settings = EditorSettings::get_global(cx);
|
||||
if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help {
|
||||
// After the code completion is finished, users often want to know what signatures are needed.
|
||||
// so we should automatically call signature_help
|
||||
self.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
apply_edits.await?;
|
||||
Ok(())
|
||||
@@ -4607,7 +4679,7 @@ impl Editor {
|
||||
let project = workspace.project().clone();
|
||||
let editor =
|
||||
cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, cx));
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.highlight_background::<Self>(
|
||||
&ranges_to_highlight,
|
||||
@@ -5067,6 +5139,23 @@ impl Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_close_hunk_diff_button(
|
||||
&self,
|
||||
hunk: HoveredHunk,
|
||||
row: DisplayRow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> IconButton {
|
||||
IconButton::new(
|
||||
("close_hunk_diff_indicator", row.0 as usize),
|
||||
ui::IconName::Close,
|
||||
)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(|cx| Tooltip::for_action("Close hunk diff", &ToggleHunkDiff, cx))
|
||||
.on_click(cx.listener(move |editor, _e, cx| editor.toggle_hovered_hunk(&hunk, cx)))
|
||||
}
|
||||
|
||||
pub fn context_menu_visible(&self) -> bool {
|
||||
self.context_menu
|
||||
.read()
|
||||
@@ -5328,6 +5417,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
this.signature_help_state.set_backspace_pressed(true);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.insert("", cx);
|
||||
let empty_str: Arc<str> = Arc::from("");
|
||||
@@ -5820,22 +5910,7 @@ impl Editor {
|
||||
let revert_changes = self.gather_revert_changes(&self.selections.disjoint_anchors(), cx);
|
||||
if !revert_changes.is_empty() {
|
||||
self.transact(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |multi_buffer, cx| {
|
||||
for (buffer_id, changes) in revert_changes {
|
||||
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
changes.into_iter().map(|(range, text)| {
|
||||
(range, text.to_string().map(Arc::<str>::from))
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
editor.change_selections(None, cx, |selections| selections.refresh());
|
||||
editor.revert(revert_changes, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5865,22 +5940,20 @@ impl Editor {
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>> {
|
||||
let mut revert_changes = HashMap::default();
|
||||
self.buffer.update(cx, |multi_buffer, cx| {
|
||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||
for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) {
|
||||
Self::prepare_revert_change(&mut revert_changes, &multi_buffer, &hunk, cx);
|
||||
}
|
||||
});
|
||||
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) {
|
||||
Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx);
|
||||
}
|
||||
revert_changes
|
||||
}
|
||||
|
||||
fn prepare_revert_change(
|
||||
pub fn prepare_revert_change(
|
||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
||||
multi_buffer: &MultiBuffer,
|
||||
multi_buffer: &Model<MultiBuffer>,
|
||||
hunk: &DiffHunk<MultiBufferRow>,
|
||||
cx: &mut AppContext,
|
||||
cx: &AppContext,
|
||||
) -> Option<()> {
|
||||
let buffer = multi_buffer.buffer(hunk.buffer_id)?;
|
||||
let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?;
|
||||
let buffer = buffer.read(cx);
|
||||
let original_text = buffer.diff_base()?.slice(hunk.diff_base_byte_range.clone());
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
@@ -8487,7 +8560,7 @@ impl Editor {
|
||||
) -> Vec<(TaskSourceKind, TaskTemplate)> {
|
||||
let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
|
||||
let (worktree_id, file) = project
|
||||
.buffer_for_id(runnable.buffer)
|
||||
.buffer_for_id(runnable.buffer, cx)
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.map(|file| (WorktreeId::from_usize(file.worktree_id()), file.clone()))
|
||||
.unzip();
|
||||
@@ -9041,7 +9114,13 @@ impl Editor {
|
||||
workspace.active_pane().clone()
|
||||
};
|
||||
|
||||
workspace.open_project_item(pane, target.buffer.clone(), cx)
|
||||
workspace.open_project_item(
|
||||
pane,
|
||||
target.buffer.clone(),
|
||||
true,
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
target_editor.update(cx, |target_editor, cx| {
|
||||
// When selecting a definition in a different buffer, disable the nav history
|
||||
@@ -9339,7 +9418,7 @@ impl Editor {
|
||||
None
|
||||
}
|
||||
});
|
||||
workspace.add_item_to_active_pane(item.clone(), destination_index, cx);
|
||||
workspace.add_item_to_active_pane(item.clone(), destination_index, true, cx);
|
||||
}
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.set_preview_item_id(Some(item_id), cx);
|
||||
@@ -9447,6 +9526,11 @@ impl Editor {
|
||||
}
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&rename_editor, |_, _, e, cx| match e {
|
||||
EditorEvent::Focused => cx.emit(EditorEvent::FocusedIn),
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let write_highlights =
|
||||
this.clear_background_highlights::<DocumentHighlightWrite>(cx);
|
||||
@@ -9720,7 +9804,7 @@ impl Editor {
|
||||
*block_id,
|
||||
(
|
||||
None,
|
||||
diagnostic_block_renderer(diagnostic.clone(), is_valid),
|
||||
diagnostic_block_renderer(diagnostic.clone(), None, true, is_valid),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -9773,7 +9857,7 @@ impl Editor {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer.anchor_after(entry.range.start),
|
||||
height: message_height,
|
||||
render: diagnostic_block_renderer(diagnostic, true),
|
||||
render: diagnostic_block_renderer(diagnostic, None, true, true),
|
||||
disposition: BlockDisposition::Below,
|
||||
}
|
||||
}),
|
||||
@@ -10073,7 +10157,7 @@ impl Editor {
|
||||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<BlockId> {
|
||||
) -> Vec<CustomBlockId> {
|
||||
let blocks = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
|
||||
@@ -10085,7 +10169,7 @@ impl Editor {
|
||||
|
||||
pub fn replace_blocks(
|
||||
&mut self,
|
||||
blocks: HashMap<BlockId, (Option<u8>, RenderBlock)>,
|
||||
blocks: HashMap<CustomBlockId, (Option<u8>, RenderBlock)>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
@@ -10098,7 +10182,7 @@ impl Editor {
|
||||
|
||||
pub fn remove_blocks(
|
||||
&mut self,
|
||||
block_ids: HashSet<BlockId>,
|
||||
block_ids: HashSet<CustomBlockId>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
@@ -10112,13 +10196,21 @@ impl Editor {
|
||||
|
||||
pub fn row_for_block(
|
||||
&self,
|
||||
block_id: BlockId,
|
||||
block_id: CustomBlockId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<DisplayRow> {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.row_for_block(block_id, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn set_focused_block(&mut self, focused_block: FocusedBlock) {
|
||||
self.focused_block = Some(focused_block);
|
||||
}
|
||||
|
||||
pub(crate) fn take_focused_block(&mut self) -> Option<FocusedBlock> {
|
||||
self.focused_block.take()
|
||||
}
|
||||
|
||||
pub fn insert_creases(
|
||||
&mut self,
|
||||
creases: impl IntoIterator<Item = Crease>,
|
||||
@@ -10332,6 +10424,22 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn working_directory(&self, cx: &WindowContext) -> Option<PathBuf> {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
if let Some(dir) = file.abs_path(cx).parent() {
|
||||
return Some(dir.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(project_path) = buffer.read(cx).project_path(cx) {
|
||||
return Some(project_path.path.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
@@ -11201,8 +11309,11 @@ impl Editor {
|
||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||
|
||||
let project_settings = ProjectSettings::get_global(cx);
|
||||
self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers;
|
||||
|
||||
if self.mode == EditorMode::Full {
|
||||
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
|
||||
let inline_blame_enabled = project_settings.git.inline_blame_enabled();
|
||||
if self.git_blame_inline_enabled != inline_blame_enabled {
|
||||
self.toggle_git_blame_inline_internal(false, cx);
|
||||
}
|
||||
@@ -11266,7 +11377,8 @@ impl Editor {
|
||||
};
|
||||
|
||||
for (buffer, ranges) in new_selections_by_buffer {
|
||||
let editor = workspace.open_project_item::<Self>(pane.clone(), buffer, cx);
|
||||
let editor =
|
||||
workspace.open_project_item::<Self>(pane.clone(), buffer, true, true, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
s.select_ranges(ranges);
|
||||
@@ -11590,6 +11702,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(EditorEvent::FocusedIn)
|
||||
}
|
||||
|
||||
fn handle_focus_out(&mut self, event: FocusOutEvent, _cx: &mut ViewContext<Self>) {
|
||||
if event.blurred != self.focus_handle {
|
||||
self.last_focused_descendant = Some(event.blurred);
|
||||
@@ -11604,8 +11720,11 @@ impl Editor {
|
||||
if let Some(blame) = self.blame.as_ref() {
|
||||
blame.update(cx, GitBlame::blur)
|
||||
}
|
||||
if !self.hover_state.focused(cx) {
|
||||
hide_hover(self, cx);
|
||||
}
|
||||
|
||||
self.hide_context_menu(cx);
|
||||
hide_hover(self, cx);
|
||||
cx.emit(EditorEvent::Blurred);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -11640,15 +11759,81 @@ impl Editor {
|
||||
pub fn file_header_size(&self) -> u8 {
|
||||
self.file_header_size
|
||||
}
|
||||
|
||||
pub fn revert(
|
||||
&mut self,
|
||||
revert_changes: HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.buffer().update(cx, |multi_buffer, cx| {
|
||||
for (buffer_id, changes) in revert_changes {
|
||||
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
changes.into_iter().map(|(range, text)| {
|
||||
(range, text.to_string().map(Arc::<str>::from))
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
self.change_selections(None, cx, |selections| selections.refresh());
|
||||
}
|
||||
|
||||
pub fn to_pixel_point(
|
||||
&mut self,
|
||||
source: multi_buffer::Anchor,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<gpui::Point<Pixels>> {
|
||||
let text_layout_details = self.text_layout_details(cx);
|
||||
let line_height = text_layout_details
|
||||
.editor_style
|
||||
.text
|
||||
.line_height_in_pixels(cx.rem_size());
|
||||
let source_point = source.to_display_point(editor_snapshot);
|
||||
let first_visible_line = text_layout_details
|
||||
.scroll_anchor
|
||||
.anchor
|
||||
.to_display_point(editor_snapshot);
|
||||
if first_visible_line > source_point {
|
||||
return None;
|
||||
}
|
||||
let source_x = editor_snapshot.x_for_display_point(source_point, &text_layout_details);
|
||||
let source_y = line_height
|
||||
* ((source_point.row() - first_visible_line.row()).0 as f32
|
||||
- text_layout_details.scroll_anchor.offset.y);
|
||||
Some(gpui::Point::new(source_x, source_y))
|
||||
}
|
||||
|
||||
pub fn display_to_pixel_point(
|
||||
&mut self,
|
||||
source: DisplayPoint,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<gpui::Point<Pixels>> {
|
||||
let line_height = self.style()?.text.line_height_in_pixels(cx.rem_size());
|
||||
let text_layout_details = self.text_layout_details(cx);
|
||||
let first_visible_line = text_layout_details
|
||||
.scroll_anchor
|
||||
.anchor
|
||||
.to_display_point(editor_snapshot);
|
||||
if first_visible_line > source {
|
||||
return None;
|
||||
}
|
||||
let source_x = editor_snapshot.x_for_display_point(source, &text_layout_details);
|
||||
let source_y = line_height * (source.row() - first_visible_line.row()).0 as f32;
|
||||
Some(gpui::Point::new(source_x, source_y))
|
||||
}
|
||||
}
|
||||
|
||||
fn hunks_for_selections(
|
||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||
selections: &[Selection<Anchor>],
|
||||
) -> Vec<DiffHunk<MultiBufferRow>> {
|
||||
let mut hunks = Vec::with_capacity(selections.len());
|
||||
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
|
||||
HashMap::default();
|
||||
let buffer_rows_for_selections = selections.iter().map(|selection| {
|
||||
let head = selection.head();
|
||||
let tail = selection.tail();
|
||||
@@ -11661,7 +11846,17 @@ fn hunks_for_selections(
|
||||
}
|
||||
});
|
||||
|
||||
for selected_multi_buffer_rows in buffer_rows_for_selections {
|
||||
hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot)
|
||||
}
|
||||
|
||||
pub fn hunks_for_rows(
|
||||
rows: impl Iterator<Item = Range<MultiBufferRow>>,
|
||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||
) -> Vec<DiffHunk<MultiBufferRow>> {
|
||||
let mut hunks = Vec::new();
|
||||
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
|
||||
HashMap::default();
|
||||
for selected_multi_buffer_rows in rows {
|
||||
let query_rows =
|
||||
selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row();
|
||||
for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) {
|
||||
@@ -12191,6 +12386,7 @@ pub enum EditorEvent {
|
||||
},
|
||||
Reparsed(BufferId),
|
||||
Focused,
|
||||
FocusedIn,
|
||||
Blurred,
|
||||
DirtyChanged,
|
||||
Saved,
|
||||
@@ -12639,8 +12835,14 @@ impl InvalidationRegion for SnippetState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> RenderBlock {
|
||||
let (text_without_backticks, code_ranges) = highlight_diagnostic_message(&diagnostic);
|
||||
pub fn diagnostic_block_renderer(
|
||||
diagnostic: Diagnostic,
|
||||
max_message_rows: Option<u8>,
|
||||
allow_closing: bool,
|
||||
_is_valid: bool,
|
||||
) -> RenderBlock {
|
||||
let (text_without_backticks, code_ranges) =
|
||||
highlight_diagnostic_message(&diagnostic, max_message_rows);
|
||||
|
||||
Box::new(move |cx: &mut BlockContext| {
|
||||
let group_id: SharedString = cx.block_id.to_string().into();
|
||||
@@ -12655,23 +12857,25 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
|
||||
let multi_line_diagnostic = diagnostic.message.contains('\n');
|
||||
|
||||
let buttons = |diagnostic: &Diagnostic, block_id: usize| {
|
||||
let buttons = |diagnostic: &Diagnostic, block_id: BlockId| {
|
||||
if multi_line_diagnostic {
|
||||
v_flex()
|
||||
} else {
|
||||
h_flex()
|
||||
}
|
||||
.children(diagnostic.is_primary.then(|| {
|
||||
IconButton::new(("close-block", block_id), IconName::XCircle)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.visible_on_hover(group_id.clone())
|
||||
.on_click(move |_click, cx| cx.dispatch_action(Box::new(Cancel)))
|
||||
.tooltip(|cx| Tooltip::for_action("Close Diagnostics", &Cancel, cx))
|
||||
}))
|
||||
.when(allow_closing, |div| {
|
||||
div.children(diagnostic.is_primary.then(|| {
|
||||
IconButton::new(("close-block", EntityId::from(block_id)), IconName::XCircle)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.visible_on_hover(group_id.clone())
|
||||
.on_click(move |_click, cx| cx.dispatch_action(Box::new(Cancel)))
|
||||
.tooltip(|cx| Tooltip::for_action("Close Diagnostics", &Cancel, cx))
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
IconButton::new(("copy-block", block_id), IconName::Copy)
|
||||
IconButton::new(("copy-block", EntityId::from(block_id)), IconName::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
@@ -12720,7 +12924,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
})
|
||||
}
|
||||
|
||||
pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, Vec<Range<usize>>) {
|
||||
pub fn highlight_diagnostic_message(
|
||||
diagnostic: &Diagnostic,
|
||||
mut max_message_rows: Option<u8>,
|
||||
) -> (SharedString, Vec<Range<usize>>) {
|
||||
let mut text_without_backticks = String::new();
|
||||
let mut code_ranges = Vec::new();
|
||||
|
||||
@@ -12732,18 +12939,53 @@ pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, V
|
||||
|
||||
let mut prev_offset = 0;
|
||||
let mut in_code_block = false;
|
||||
for (ix, _) in diagnostic
|
||||
let has_row_limit = max_message_rows.is_some();
|
||||
let mut newline_indices = diagnostic
|
||||
.message
|
||||
.match_indices('\n')
|
||||
.filter(|_| has_row_limit)
|
||||
.map(|(ix, _)| ix)
|
||||
.fuse()
|
||||
.peekable();
|
||||
|
||||
for (quote_ix, _) in diagnostic
|
||||
.message
|
||||
.match_indices('`')
|
||||
.chain([(diagnostic.message.len(), "")])
|
||||
{
|
||||
let mut first_newline_ix = None;
|
||||
let mut last_newline_ix = None;
|
||||
while let Some(newline_ix) = newline_indices.peek() {
|
||||
if *newline_ix < quote_ix {
|
||||
if first_newline_ix.is_none() {
|
||||
first_newline_ix = Some(*newline_ix);
|
||||
}
|
||||
last_newline_ix = Some(*newline_ix);
|
||||
|
||||
if let Some(rows_left) = &mut max_message_rows {
|
||||
if *rows_left == 0 {
|
||||
break;
|
||||
} else {
|
||||
*rows_left -= 1;
|
||||
}
|
||||
}
|
||||
let _ = newline_indices.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let prev_len = text_without_backticks.len();
|
||||
text_without_backticks.push_str(&diagnostic.message[prev_offset..ix]);
|
||||
prev_offset = ix + 1;
|
||||
let new_text = &diagnostic.message[prev_offset..first_newline_ix.unwrap_or(quote_ix)];
|
||||
text_without_backticks.push_str(new_text);
|
||||
if in_code_block {
|
||||
code_ranges.push(prev_len..text_without_backticks.len());
|
||||
}
|
||||
prev_offset = last_newline_ix.unwrap_or(quote_ix) + 1;
|
||||
in_code_block = !in_code_block;
|
||||
if first_newline_ix.map_or(false, |newline_ix| newline_ix < quote_ix) {
|
||||
text_without_backticks.push_str("...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(text_without_backticks.into(), code_ranges)
|
||||
@@ -12824,8 +13066,13 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> +
|
||||
})
|
||||
}
|
||||
|
||||
pub trait RangeToAnchorExt {
|
||||
pub trait RangeToAnchorExt: Sized {
|
||||
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
|
||||
|
||||
fn to_display_points(self, snapshot: &EditorSnapshot) -> Range<DisplayPoint> {
|
||||
let anchor_range = self.to_anchors(&snapshot.buffer_snapshot);
|
||||
anchor_range.start.to_display_point(&snapshot)..anchor_range.end.to_display_point(&snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToOffset> RangeToAnchorExt for Range<T> {
|
||||
|
||||
@@ -25,6 +25,9 @@ pub struct EditorSettings {
|
||||
pub expand_excerpt_lines: u32,
|
||||
#[serde(default)]
|
||||
pub double_click_in_multibuffer: DoubleClickInMultibuffer,
|
||||
pub search_wrap: bool,
|
||||
pub auto_signature_help: bool,
|
||||
pub show_signature_help_after_edits: bool,
|
||||
#[serde(default)]
|
||||
pub jupyter: Jupyter,
|
||||
}
|
||||
@@ -228,6 +231,20 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: select
|
||||
pub double_click_in_multibuffer: Option<DoubleClickInMultibuffer>,
|
||||
/// Whether the editor search results will loop
|
||||
///
|
||||
/// Default: true
|
||||
pub search_wrap: Option<bool>,
|
||||
|
||||
/// Whether to automatically show a signature help pop-up or not.
|
||||
///
|
||||
/// Default: false
|
||||
pub auto_signature_help: Option<bool>,
|
||||
|
||||
/// Whether to show the signature help pop-up after completions or bracket pairs inserted.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_signature_help_after_edits: Option<bool>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
pub jupyter: Option<Jupyter>,
|
||||
|
||||
@@ -21,13 +21,16 @@ use language::{
|
||||
BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
|
||||
Point,
|
||||
ParsedMarkdown, Point,
|
||||
};
|
||||
use language_settings::IndentGuideSettings;
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
use project::FakeFs;
|
||||
use project::{
|
||||
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::sync::atomic;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
@@ -6831,6 +6834,626 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions {
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "/*".to_string(),
|
||||
end: " */".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "[".to_string(),
|
||||
end: "]".to_string(),
|
||||
close: false,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "\"".to_string(),
|
||||
end: "\"".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "<".to_string(),
|
||||
end: ">".to_string(),
|
||||
close: false,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
autoclose_before: "})]".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let language = Arc::new(language);
|
||||
|
||||
cx.language_registry().add(language.clone());
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(language), cx);
|
||||
});
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
fn main() {
|
||||
sampleˇ
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input("(", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
fn main() {
|
||||
sample(ˇ)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
let signature_help_state = editor.signature_help_state.popover().cloned();
|
||||
assert!(signature_help_state.is_some());
|
||||
let ParsedMarkdown {
|
||||
text, highlights, ..
|
||||
} = signature_help_state.unwrap().parsed_content;
|
||||
assert_eq!(text, "param1: u8, param2: u8");
|
||||
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(false);
|
||||
settings.show_signature_help_after_edits = Some(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions {
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "/*".to_string(),
|
||||
end: " */".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "[".to_string(),
|
||||
end: "]".to_string(),
|
||||
close: false,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "\"".to_string(),
|
||||
end: "\"".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "<".to_string(),
|
||||
end: ">".to_string(),
|
||||
close: false,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
autoclose_before: "})]".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let language = Arc::new(language);
|
||||
|
||||
cx.language_registry().add(language.clone());
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(language), cx);
|
||||
});
|
||||
|
||||
// Ensure that signature_help is not called when no signature help is enabled.
|
||||
cx.set_state(
|
||||
&r#"
|
||||
fn main() {
|
||||
sampleˇ
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input("(", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
fn main() {
|
||||
sample(ˇ)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.signature_help_state.task().is_none());
|
||||
});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
|
||||
// Ensure that signature_help is called when enabled afte edits
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(false);
|
||||
settings.show_signature_help_after_edits = Some(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.set_state(
|
||||
&r#"
|
||||
fn main() {
|
||||
sampleˇ
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input("(", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
fn main() {
|
||||
sample(ˇ)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.update_editor(|editor, _| {
|
||||
let signature_help_state = editor.signature_help_state.popover().cloned();
|
||||
assert!(signature_help_state.is_some());
|
||||
let ParsedMarkdown {
|
||||
text, highlights, ..
|
||||
} = signature_help_state.unwrap().parsed_content;
|
||||
assert_eq!(text, "param1: u8, param2: u8");
|
||||
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
|
||||
editor.signature_help_state = SignatureHelpState::default();
|
||||
});
|
||||
|
||||
// Ensure that signature_help is called when auto signature help override is enabled
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(true);
|
||||
settings.show_signature_help_after_edits = Some(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.set_state(
|
||||
&r#"
|
||||
fn main() {
|
||||
sampleˇ
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input("(", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
fn main() {
|
||||
sample(ˇ)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.editor(|editor, _| {
|
||||
let signature_help_state = editor.signature_help_state.popover().cloned();
|
||||
assert!(signature_help_state.is_some());
|
||||
let ParsedMarkdown {
|
||||
text, highlights, ..
|
||||
} = signature_help_state.unwrap().parsed_content;
|
||||
assert_eq!(text, "param1: u8, param2: u8");
|
||||
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_signature_help(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions {
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// A test that directly calls `show_signature_help`
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.show_signature_help(&ShowSignatureHelp, cx);
|
||||
});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
let signature_help_state = editor.signature_help_state.popover().cloned();
|
||||
assert!(signature_help_state.is_some());
|
||||
let ParsedMarkdown {
|
||||
text, highlights, ..
|
||||
} = signature_help_state.unwrap().parsed_content;
|
||||
assert_eq!(text, "param1: u8, param2: u8");
|
||||
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
|
||||
});
|
||||
|
||||
// When exiting outside from inside the brackets, `signature_help` is closed.
|
||||
cx.set_state(indoc! {"
|
||||
fn main() {
|
||||
sample(ˇ);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
|
||||
});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: Vec::new(),
|
||||
active_signature: None,
|
||||
active_parameter: None,
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
|
||||
cx.condition(|editor, _| !editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
assert!(!editor.signature_help_state.is_shown());
|
||||
});
|
||||
|
||||
// When entering inside the brackets from outside, `show_signature_help` is automatically called.
|
||||
cx.set_state(indoc! {"
|
||||
fn main() {
|
||||
sample(ˇ);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.signature_help_state.is_shown());
|
||||
});
|
||||
|
||||
// Restore the popover with more parameter input
|
||||
cx.set_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, param2ˇ);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
|
||||
// When selecting a range, the popover is gone.
|
||||
// Avoid using `cx.set_state` to not actually edit the document, just change its selections.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, «ˇparam2»);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
cx.editor(|editor, _| {
|
||||
assert!(!editor.signature_help_state.is_shown());
|
||||
});
|
||||
|
||||
// When unselecting again, the popover is back if within the brackets.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, ˇparam2);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.signature_help_state.is_shown());
|
||||
});
|
||||
|
||||
// Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
|
||||
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, ˇparam2);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
|
||||
});
|
||||
cx.condition(|editor, _| !editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, «ˇparam2»);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, ˇparam2);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -8189,7 +8812,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
let follower_1 = cx
|
||||
.update_window(*workspace.deref(), |_, cx| {
|
||||
Editor::from_state_proto(
|
||||
pane.clone(),
|
||||
workspace.root_view(cx).unwrap(),
|
||||
ViewId {
|
||||
creator: Default::default(),
|
||||
@@ -8281,7 +8903,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
let follower_2 = cx
|
||||
.update_window(*workspace.deref(), |_, cx| {
|
||||
Editor::from_state_proto(
|
||||
pane.clone(),
|
||||
workspace.root_view(cx).unwrap().clone(),
|
||||
ViewId {
|
||||
creator: Default::default(),
|
||||
@@ -9824,7 +10445,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
|
||||
workspace.active_item(cx).is_none(),
|
||||
"active item should be None before the first item is added"
|
||||
);
|
||||
workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), None, cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(multi_buffer_editor.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
let active_item = workspace
|
||||
.active_item(cx)
|
||||
.expect("should have an active item after adding the multi buffer");
|
||||
@@ -12450,6 +13076,21 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
|
||||
);
|
||||
}
|
||||
|
||||
pub fn handle_signature_help_request(
|
||||
cx: &mut EditorLspTestContext,
|
||||
mocked_response: lsp::SignatureHelp,
|
||||
) -> impl Future<Output = ()> {
|
||||
let mut request =
|
||||
cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
|
||||
let mocked_response = mocked_response.clone();
|
||||
async move { Ok(Some(mocked_response)) }
|
||||
});
|
||||
|
||||
async move {
|
||||
request.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle completion request passing a marked string specifying where the completion
|
||||
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
/// should be returned using '<' and '>' to delimit the range
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,24 +5,26 @@ use crate::{
|
||||
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
|
||||
EditorStyle, Hover, RangeToAnchorExt,
|
||||
};
|
||||
use futures::{stream::FuturesUnordered, FutureExt};
|
||||
use gpui::{
|
||||
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
|
||||
Task, ViewContext, WeakView,
|
||||
div, px, AnyElement, AsyncWindowContext, CursorStyle, FontWeight, Hsla, InteractiveElement,
|
||||
IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, SharedString, Size,
|
||||
StatefulInteractiveElement, StyleRefinement, Styled, Task, TextStyleRefinement, View,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
|
||||
|
||||
use itertools::Itertools;
|
||||
use language::{DiagnosticEntry, Language, LanguageRegistry};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use multi_buffer::ToOffset;
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
|
||||
use project::{HoverBlock, InlayHintLabelPart};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
use std::rc::Rc;
|
||||
use std::{borrow::Cow, cell::RefCell};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, window_is_transparent, Tooltip};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
@@ -40,6 +42,9 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
|
||||
/// depending on whether a point to hover over is provided.
|
||||
pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) {
|
||||
if EditorSettings::get_global(cx).hover_popover_enabled {
|
||||
if show_keyboard_hover(editor, cx) {
|
||||
return;
|
||||
}
|
||||
if let Some(anchor) = anchor {
|
||||
show_hover(editor, anchor, false, cx);
|
||||
} else {
|
||||
@@ -48,6 +53,20 @@ pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContex
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_keyboard_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let info_popovers = editor.hover_state.info_popovers.clone();
|
||||
for p in info_popovers {
|
||||
let keyboard_grace = p.keyboard_grace.borrow();
|
||||
if *keyboard_grace {
|
||||
if let Some(anchor) = p.anchor {
|
||||
show_hover(editor, anchor, false, cx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub struct InlayHover {
|
||||
pub range: InlayHighlight,
|
||||
pub tooltip: HoverBlock,
|
||||
@@ -113,12 +132,14 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
|
||||
let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
|
||||
let blocks = vec![inlay_hover.tooltip];
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
|
||||
|
||||
let hover_popover = InfoPopover {
|
||||
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
keyboard_grace: Rc::new(RefCell::new(false)),
|
||||
anchor: None,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -291,39 +312,40 @@ fn show_hover(
|
||||
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
|
||||
let mut hover_highlights = Vec::with_capacity(hovers_response.len());
|
||||
let mut info_popovers = Vec::with_capacity(hovers_response.len());
|
||||
let mut info_popover_tasks = hovers_response
|
||||
.into_iter()
|
||||
.map(|hover_result| async {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = hover_result
|
||||
.range
|
||||
.and_then(|range| {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
|
||||
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| anchor..anchor);
|
||||
for hover_result in hovers_response {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = hover_result
|
||||
.range
|
||||
.and_then(|range| {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
|
||||
let blocks = hover_result.contents;
|
||||
let language = hover_result.language;
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| anchor..anchor);
|
||||
|
||||
(
|
||||
range.clone(),
|
||||
InfoPopover {
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
while let Some((highlight_range, info_popover)) = info_popover_tasks.next().await {
|
||||
let blocks = hover_result.contents;
|
||||
let language = hover_result.language;
|
||||
let parsed_content =
|
||||
parse_blocks(&blocks, &language_registry, language, &mut cx).await;
|
||||
info_popover_tasks.push((
|
||||
range.clone(),
|
||||
InfoPopover {
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
|
||||
anchor: Some(anchor),
|
||||
},
|
||||
));
|
||||
}
|
||||
for (highlight_range, info_popover) in info_popover_tasks {
|
||||
hover_highlights.push(highlight_range);
|
||||
info_popovers.push(info_popover);
|
||||
}
|
||||
@@ -357,72 +379,81 @@ async fn parse_blocks(
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
) -> markdown::ParsedMarkdown {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Option<View<Markdown>> {
|
||||
let fallback_language_name = if let Some(ref l) = language {
|
||||
let l = Arc::clone(l);
|
||||
Some(l.lsp_id().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for block in blocks {
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
markdown::new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text.replace("\\n", "\n"));
|
||||
let combined_text = blocks
|
||||
.iter()
|
||||
.map(|block| match &block.kind {
|
||||
project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
|
||||
Cow::Borrowed(block.text.trim())
|
||||
}
|
||||
|
||||
HoverBlockKind::Markdown => {
|
||||
markdown::parse_markdown_block(
|
||||
&block.text.replace("\\n", "\n"),
|
||||
language_registry,
|
||||
language.clone(),
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut region_ranges,
|
||||
&mut regions,
|
||||
)
|
||||
.await
|
||||
project::HoverBlockKind::Code { language } => {
|
||||
Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
|
||||
}
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
HoverBlockKind::Code { language } => {
|
||||
if let Some(language) = language_registry
|
||||
.language_for_name(language)
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
{
|
||||
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
|
||||
} else {
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rendered_block = cx
|
||||
.new_view(|cx| {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let buffer_font_family = settings.buffer_font.family.clone();
|
||||
let mut base_style = cx.text_style();
|
||||
base_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(buffer_font_family.clone()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let leading_space = text.chars().take_while(|c| c.is_whitespace()).count();
|
||||
if leading_space > 0 {
|
||||
highlights = highlights
|
||||
.into_iter()
|
||||
.map(|(range, style)| {
|
||||
(
|
||||
range.start.saturating_sub(leading_space)
|
||||
..range.end.saturating_sub(leading_space),
|
||||
style,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
region_ranges = region_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: base_style,
|
||||
code_block: StyleRefinement::default().mt(rems(1.)).mb(rems(1.)),
|
||||
inline_code: TextStyleRefinement {
|
||||
background_color: Some(cx.theme().colors().background),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Color::Muted.color(cx),
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: TextStyleRefinement {
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: TextStyleRefinement {
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
break_style: Default::default(),
|
||||
heading: StyleRefinement::default()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.text_base()
|
||||
.mt(rems(1.))
|
||||
.mb_0(),
|
||||
};
|
||||
|
||||
ParsedMarkdown {
|
||||
text: text.trim().to_string(),
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
Markdown::new(
|
||||
combined_text,
|
||||
markdown_style.clone(),
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
fallback_language_name,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
|
||||
rendered_block
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -444,7 +475,7 @@ impl HoverState {
|
||||
style: &EditorStyle,
|
||||
visible_rows: Range<DisplayRow>,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<AnyElement>)> {
|
||||
// If there is a diagnostic, position the popovers based on that.
|
||||
@@ -482,29 +513,39 @@ impl HoverState {
|
||||
elements.push(diagnostic_popover.render(style, max_size, cx));
|
||||
}
|
||||
for info_popover in &mut self.info_popovers {
|
||||
elements.push(info_popover.render(style, max_size, workspace.clone(), cx));
|
||||
elements.push(info_popover.render(max_size, cx));
|
||||
}
|
||||
|
||||
Some((point, elements))
|
||||
}
|
||||
|
||||
pub fn focused(&self, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let mut hover_popover_is_focused = false;
|
||||
for info_popover in &self.info_popovers {
|
||||
for markdown_view in &info_popover.parsed_content {
|
||||
if markdown_view.focus_handle(cx).is_focused(cx) {
|
||||
hover_popover_is_focused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hover_popover_is_focused;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
pub struct InfoPopover {
|
||||
pub symbol_range: RangeInEditor,
|
||||
pub parsed_content: ParsedMarkdown,
|
||||
pub parsed_content: Option<View<Markdown>>,
|
||||
pub scroll_handle: ScrollHandle,
|
||||
pub keyboard_grace: Rc<RefCell<bool>>,
|
||||
pub anchor: Option<Anchor>,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
pub fn render(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
|
||||
let keyboard_grace = Rc::clone(&self.keyboard_grace);
|
||||
let mut d = div()
|
||||
.id("info_popover")
|
||||
.elevation_2(cx)
|
||||
.overflow_y_scroll()
|
||||
@@ -514,15 +555,17 @@ impl InfoPopover {
|
||||
// Prevent a mouse down/move on the popover from being propagated to the editor,
|
||||
// because that would dismiss the popover.
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(div().p_2().child(crate::render_parsed_markdown(
|
||||
"content",
|
||||
&self.parsed_content,
|
||||
style,
|
||||
workspace,
|
||||
cx,
|
||||
)))
|
||||
.into_any_element()
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
let mut keyboard_grace = keyboard_grace.borrow_mut();
|
||||
*keyboard_grace = false;
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.p_2();
|
||||
|
||||
if let Some(markdown) = &self.parsed_content {
|
||||
d = d.child(markdown.clone());
|
||||
}
|
||||
d.into_any_element()
|
||||
}
|
||||
|
||||
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
|
||||
@@ -593,8 +636,6 @@ impl DiagnosticPopover {
|
||||
.when(window_is_transparent(cx), |this| {
|
||||
this.bg(gpui::transparent_black())
|
||||
})
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
|
||||
// Prevent a mouse move on the popover from being propagated to the editor,
|
||||
@@ -608,6 +649,8 @@ impl DiagnosticPopover {
|
||||
div()
|
||||
.id("diagnostic-inner")
|
||||
.overflow_y_scroll()
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(diagnostic_colors.background)
|
||||
@@ -642,17 +685,33 @@ mod tests {
|
||||
InlayId, PointForPosition,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use markdown::parser::MarkdownEvent;
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::atomic;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use text::Bias;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
impl InfoPopover {
|
||||
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
|
||||
let mut rendered_text = String::new();
|
||||
if let Some(parsed_content) = self.parsed_content.clone() {
|
||||
let markdown = parsed_content.read(cx);
|
||||
let text = markdown.parsed_markdown().source().to_string();
|
||||
let data = markdown.parsed_markdown().events();
|
||||
let slice = data;
|
||||
|
||||
for (range, event) in slice.iter() {
|
||||
if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
|
||||
rendered_text.push_str(&text[range.clone()])
|
||||
}
|
||||
}
|
||||
}
|
||||
rendered_text
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mouse_hover_info_popover_with_autocomplete_popover(
|
||||
@@ -736,7 +795,7 @@ mod tests {
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
requests.next().await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -744,14 +803,13 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// check that the completion menu is still visible and that there still has only been 1 completion request
|
||||
@@ -777,7 +835,7 @@ mod tests {
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
|
||||
//verify the information popover is still visible and unchanged
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -785,14 +843,14 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// Mouse moved with no hover response dismisses
|
||||
@@ -870,7 +928,7 @@ mod tests {
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
requests.next().await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -878,14 +936,14 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// Mouse moved with no hover response dismisses
|
||||
@@ -931,34 +989,49 @@ mod tests {
|
||||
let symbol_range = cx.lsp_range(indoc! {"
|
||||
«fn» test() { println!(); }
|
||||
"});
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: "some other basic docs".to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _cx| {
|
||||
assert!(!editor.hover_state.visible());
|
||||
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
0,
|
||||
"Expected no hovers but got but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
});
|
||||
|
||||
let mut requests =
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: "some other basic docs".to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
});
|
||||
|
||||
requests.next().await;
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some other basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some other basic docs".to_string())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -998,24 +1071,25 @@ mod tests {
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
rendered_text,
|
||||
"regular text for hover to show".to_string(),
|
||||
"No empty string hovers should be shown"
|
||||
);
|
||||
@@ -1063,24 +1137,25 @@ mod tests {
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
code_str.trim(),
|
||||
rendered_text, code_str,
|
||||
"Should not have extra line breaks at end of rendered hover"
|
||||
);
|
||||
});
|
||||
@@ -1156,153 +1231,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let editor = cx.add_window(|cx| Editor::single_line(cx));
|
||||
editor
|
||||
.update(cx, |editor, _cx| {
|
||||
let style = editor.style.clone().unwrap();
|
||||
|
||||
struct Row {
|
||||
blocks: Vec<HoverBlock>,
|
||||
expected_marked_text: String,
|
||||
expected_styles: Vec<HighlightStyle>,
|
||||
}
|
||||
|
||||
let rows = &[
|
||||
// Strong emphasis
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "one **two** three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Links
|
||||
Row {
|
||||
blocks: vec three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Lists
|
||||
Row {
|
||||
blocks: vec
|
||||
- d"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
lists:
|
||||
- one
|
||||
- a
|
||||
- b
|
||||
- two
|
||||
- «c»
|
||||
- d"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Multi-paragraph list items
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "
|
||||
* one two
|
||||
three
|
||||
|
||||
* four five
|
||||
* six seven
|
||||
eight
|
||||
|
||||
nine
|
||||
* ten
|
||||
* six"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
- one two three
|
||||
- four five
|
||||
- six seven eight
|
||||
|
||||
nine
|
||||
- ten
|
||||
- six"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
for Row {
|
||||
blocks,
|
||||
expected_marked_text,
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
.into_iter()
|
||||
.zip(expected_styles.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
rendered.text, expected_text,
|
||||
"wrong text for input {blocks:?}"
|
||||
);
|
||||
|
||||
let rendered_highlights: Vec<_> = rendered
|
||||
.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight)| {
|
||||
let highlight = highlight.to_highlight_style(&style.syntax)?;
|
||||
Some((range.clone(), highlight))
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
rendered_highlights, expected_highlights,
|
||||
"wrong highlights for input {blocks:?}"
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
@@ -1546,9 +1474,8 @@ mod tests {
|
||||
"Popover range should match the new type label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover.parsed_content.text,
|
||||
format!("A tooltip for `{new_type_label}`"),
|
||||
"Rendered text should not anyhow alter backticks"
|
||||
popover.get_rendered_text(cx),
|
||||
format!("A tooltip for {new_type_label}"),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1602,7 +1529,7 @@ mod tests {
|
||||
"Popover range should match the struct label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover.parsed_content.text,
|
||||
popover.get_rendered_text(cx),
|
||||
format!("A tooltip for {struct_label}"),
|
||||
"Rendered markdown element should remove backticks from text"
|
||||
);
|
||||
|
||||
@@ -5,28 +5,31 @@ use std::{
|
||||
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use gpui::{AppContext, Hsla, Model, Task, View};
|
||||
use gpui::{Action, AppContext, Hsla, Model, MouseButton, Subscription, Task, View};
|
||||
use language::Buffer;
|
||||
use multi_buffer::{
|
||||
Anchor, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
|
||||
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use text::{BufferId, Point};
|
||||
use ui::{
|
||||
div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext,
|
||||
h_flex, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, IntoElement,
|
||||
ParentElement, Pixels, Styled, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{debug_panic, RangeExt};
|
||||
|
||||
use crate::{
|
||||
editor_settings::CurrentLineHighlight,
|
||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||
hunk_status, hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle,
|
||||
DiffRowHighlight, Editor, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt,
|
||||
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
|
||||
hunk_status, hunks_for_selections,
|
||||
mouse_context_menu::MouseContextMenu,
|
||||
BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, Editor,
|
||||
EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks, ToDisplayPoint,
|
||||
ToggleHunkDiff,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct HunkToExpand {
|
||||
pub(super) struct HoveredHunk {
|
||||
pub multi_buffer_range: Range<Anchor>,
|
||||
pub status: DiffHunkStatus,
|
||||
pub diff_base_byte_range: Range<usize>,
|
||||
@@ -55,7 +58,7 @@ impl ExpandedHunks {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct ExpandedHunk {
|
||||
pub block: Option<BlockId>,
|
||||
pub block: Option<CustomBlockId>,
|
||||
pub hunk_range: Range<Anchor>,
|
||||
pub diff_base_byte_range: Range<usize>,
|
||||
pub status: DiffHunkStatus,
|
||||
@@ -63,6 +66,123 @@ pub(super) struct ExpandedHunk {
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub(super) fn open_hunk_context_menu(
|
||||
&mut self,
|
||||
hovered_hunk: HoveredHunk,
|
||||
clicked_point: gpui::Point<Pixels>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let expanded = self
|
||||
.expanded_hunks
|
||||
.hunks(false)
|
||||
.any(|expanded_hunk| expanded_hunk.hunk_range == hovered_hunk.multi_buffer_range);
|
||||
let editor_handle = cx.view().clone();
|
||||
let editor_snapshot = self.snapshot(cx);
|
||||
let start_point = self
|
||||
.to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx)
|
||||
.unwrap_or(clicked_point);
|
||||
let end_point = self
|
||||
.to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx)
|
||||
.unwrap_or(clicked_point);
|
||||
let norm =
|
||||
|a: gpui::Point<Pixels>, b: gpui::Point<Pixels>| (a.x - b.x).abs() + (a.y - b.y).abs();
|
||||
let closest_source = if norm(start_point, clicked_point) < norm(end_point, clicked_point) {
|
||||
hovered_hunk.multi_buffer_range.start
|
||||
} else {
|
||||
hovered_hunk.multi_buffer_range.end
|
||||
};
|
||||
|
||||
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
self,
|
||||
closest_source,
|
||||
clicked_point,
|
||||
ContextMenu::build(cx, move |menu, _| {
|
||||
menu.on_blur_subscription(Subscription::new(|| {}))
|
||||
.context(focus_handle)
|
||||
.entry(
|
||||
if expanded {
|
||||
"Collapse Hunk"
|
||||
} else {
|
||||
"Expand Hunk"
|
||||
},
|
||||
Some(ToggleHunkDiff.boxed_clone()),
|
||||
{
|
||||
let editor = editor_handle.clone();
|
||||
let hunk = hovered_hunk.clone();
|
||||
move |cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_hovered_hunk(&hunk, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.entry("Revert Hunk", Some(RevertSelectedHunks.boxed_clone()), {
|
||||
let editor = editor_handle.clone();
|
||||
let hunk = hovered_hunk.clone();
|
||||
move |cx| {
|
||||
let multi_buffer = editor.read(cx).buffer().clone();
|
||||
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
|
||||
let mut revert_changes = HashMap::default();
|
||||
if let Some(hunk) =
|
||||
crate::hunk_diff::to_diff_hunk(&hunk, &multi_buffer_snapshot)
|
||||
{
|
||||
Editor::prepare_revert_change(
|
||||
&mut revert_changes,
|
||||
&multi_buffer,
|
||||
&hunk,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
if !revert_changes.is_empty() {
|
||||
editor.update(cx, |editor, cx| editor.revert(revert_changes, cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.entry("Revert File", None, {
|
||||
let editor = editor_handle.clone();
|
||||
move |cx| {
|
||||
let mut revert_changes = HashMap::default();
|
||||
let multi_buffer = editor.read(cx).buffer().clone();
|
||||
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
|
||||
for hunk in crate::hunks_for_rows(
|
||||
Some(MultiBufferRow(0)..multi_buffer_snapshot.max_buffer_row())
|
||||
.into_iter(),
|
||||
&multi_buffer_snapshot,
|
||||
) {
|
||||
Editor::prepare_revert_change(
|
||||
&mut revert_changes,
|
||||
&multi_buffer,
|
||||
&hunk,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
if !revert_changes.is_empty() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.revert(revert_changes, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn toggle_hovered_hunk(
|
||||
&mut self,
|
||||
hovered_hunk: &HoveredHunk,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let editor_snapshot = self.snapshot(cx);
|
||||
if let Some(diff_hunk) = to_diff_hunk(hovered_hunk, &editor_snapshot.buffer_snapshot) {
|
||||
self.toggle_hunks_expanded(vec![diff_hunk], cx);
|
||||
self.change_selections(None, cx, |selections| selections.refresh());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let selections = self.selections.disjoint_anchors();
|
||||
@@ -164,7 +284,7 @@ impl Editor {
|
||||
retain = false;
|
||||
break;
|
||||
} else {
|
||||
hunks_to_expand.push(HunkToExpand {
|
||||
hunks_to_expand.push(HoveredHunk {
|
||||
status,
|
||||
multi_buffer_range,
|
||||
diff_base_byte_range,
|
||||
@@ -182,7 +302,7 @@ impl Editor {
|
||||
let remaining_hunk_point_range =
|
||||
Point::new(remaining_hunk.associated_range.start.0, 0)
|
||||
..Point::new(remaining_hunk.associated_range.end.0, 0);
|
||||
hunks_to_expand.push(HunkToExpand {
|
||||
hunks_to_expand.push(HoveredHunk {
|
||||
status: hunk_status(&remaining_hunk),
|
||||
multi_buffer_range: remaining_hunk_point_range
|
||||
.to_anchors(&snapshot.buffer_snapshot),
|
||||
@@ -215,7 +335,7 @@ impl Editor {
|
||||
pub(super) fn expand_diff_hunk(
|
||||
&mut self,
|
||||
diff_base_buffer: Option<Model<Buffer>>,
|
||||
hunk: &HunkToExpand,
|
||||
hunk: &HoveredHunk,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<()> {
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
@@ -303,28 +423,58 @@ impl Editor {
|
||||
&mut self,
|
||||
diff_base_buffer: Model<Buffer>,
|
||||
deleted_text_height: u8,
|
||||
hunk: &HunkToExpand,
|
||||
hunk: &HoveredHunk,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) -> Option<BlockId> {
|
||||
) -> Option<CustomBlockId> {
|
||||
let deleted_hunk_color = deleted_hunk_color(cx);
|
||||
let (editor_height, editor_with_deleted_text) =
|
||||
editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
|
||||
let editor = cx.view().clone();
|
||||
let editor_model = cx.model().clone();
|
||||
let hunk = hunk.clone();
|
||||
let mut new_block_ids = self.insert_blocks(
|
||||
Some(BlockProperties {
|
||||
position: hunk.multi_buffer_range.start,
|
||||
height: editor_height.max(deleted_text_height),
|
||||
style: BlockStyle::Flex,
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Box::new(move |cx| {
|
||||
let close_button = editor.update(cx.context, |editor, cx| {
|
||||
let editor_snapshot = editor.snapshot(cx);
|
||||
let hunk_start_row = hunk
|
||||
.multi_buffer_range
|
||||
.start
|
||||
.to_display_point(&editor_snapshot)
|
||||
.row();
|
||||
editor.render_close_hunk_diff_button(hunk.clone(), hunk_start_row, cx)
|
||||
});
|
||||
let gutter_dimensions = editor_model.read(cx).gutter_dimensions;
|
||||
div()
|
||||
let click_editor = editor.clone();
|
||||
h_flex()
|
||||
.bg(deleted_hunk_color)
|
||||
.size_full()
|
||||
.pl(gutter_dimensions.full_width())
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_center()
|
||||
.max_w(gutter_dimensions.full_width())
|
||||
.min_w(gutter_dimensions.full_width())
|
||||
.size_full()
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let click_hunk = hunk.clone();
|
||||
move |e, cx| {
|
||||
let modifiers = e.modifiers;
|
||||
if modifiers.control || modifiers.platform {
|
||||
click_editor.update(cx, |editor, cx| {
|
||||
editor.toggle_hovered_hunk(&click_hunk, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(close_button),
|
||||
)
|
||||
.child(editor_with_deleted_text.clone())
|
||||
.into_any_element()
|
||||
}),
|
||||
disposition: BlockDisposition::Above,
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
@@ -339,16 +489,21 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) {
|
||||
pub(super) fn clear_clicked_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
|
||||
self.expanded_hunks.hunk_update_tasks.clear();
|
||||
self.clear_row_highlights::<DiffRowHighlight>();
|
||||
let to_remove = self
|
||||
.expanded_hunks
|
||||
.hunks
|
||||
.drain(..)
|
||||
.filter_map(|expanded_hunk| expanded_hunk.block)
|
||||
.collect();
|
||||
self.clear_row_highlights::<DiffRowHighlight>();
|
||||
self.remove_blocks(to_remove, None, cx);
|
||||
.collect::<HashSet<_>>();
|
||||
if to_remove.is_empty() {
|
||||
false
|
||||
} else {
|
||||
self.remove_blocks(to_remove, None, cx);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn sync_expanded_diff_hunks(
|
||||
@@ -457,7 +612,7 @@ impl Editor {
|
||||
recalculated_hunks.next();
|
||||
retain = true;
|
||||
} else {
|
||||
hunks_to_reexpand.push(HunkToExpand {
|
||||
hunks_to_reexpand.push(HoveredHunk {
|
||||
status,
|
||||
multi_buffer_range,
|
||||
diff_base_byte_range,
|
||||
@@ -522,6 +677,29 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_diff_hunk(
|
||||
hovered_hunk: &HoveredHunk,
|
||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||
) -> Option<DiffHunk<MultiBufferRow>> {
|
||||
let buffer_id = hovered_hunk
|
||||
.multi_buffer_range
|
||||
.start
|
||||
.buffer_id
|
||||
.or_else(|| hovered_hunk.multi_buffer_range.end.buffer_id)?;
|
||||
let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor
|
||||
..hovered_hunk.multi_buffer_range.end.text_anchor;
|
||||
let point_range = hovered_hunk
|
||||
.multi_buffer_range
|
||||
.to_point(&multi_buffer_snapshot);
|
||||
Some(DiffHunk {
|
||||
associated_range: MultiBufferRow(point_range.start.row)
|
||||
..MultiBufferRow(point_range.end.row),
|
||||
buffer_id,
|
||||
buffer_range,
|
||||
diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_diff_base_buffer(buffer: &Model<Buffer>, cx: &mut AppContext) -> Option<Model<Buffer>> {
|
||||
buffer
|
||||
.update(cx, |buffer, _| {
|
||||
@@ -555,7 +733,7 @@ fn deleted_hunk_color(cx: &AppContext) -> Hsla {
|
||||
fn editor_with_deleted_text(
|
||||
diff_base_buffer: Model<Buffer>,
|
||||
deleted_color: Hsla,
|
||||
hunk: &HunkToExpand,
|
||||
hunk: &HoveredHunk,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> (u8, View<Editor>) {
|
||||
let parent_editor = cx.view().downgrade();
|
||||
@@ -613,11 +791,12 @@ fn editor_with_deleted_text(
|
||||
}
|
||||
}),
|
||||
]);
|
||||
let parent_editor_for_reverts = parent_editor.clone();
|
||||
let original_multi_buffer_range = hunk.multi_buffer_range.clone();
|
||||
let diff_base_range = hunk.diff_base_byte_range.clone();
|
||||
editor
|
||||
.register_action::<RevertSelectedHunks>(move |_, cx| {
|
||||
parent_editor
|
||||
parent_editor_for_reverts
|
||||
.update(cx, |editor, cx| {
|
||||
let Some((buffer, original_text)) =
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
@@ -645,6 +824,16 @@ fn editor_with_deleted_text(
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
let hunk = hunk.clone();
|
||||
editor
|
||||
.register_action::<ToggleHunkDiff>(move |_, cx| {
|
||||
parent_editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.toggle_hovered_hunk(&hunk, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
editor
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use file_icons::FileIcons;
|
||||
use futures::future::try_join_all;
|
||||
use git::repository::GitFileStatus;
|
||||
use gpui::{
|
||||
@@ -16,10 +17,13 @@ use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
||||
};
|
||||
use multi_buffer::AnchorRangeExt;
|
||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use project::{
|
||||
project_settings::ProjectSettings, search::SearchQuery, FormatTrigger, Item as _, Project,
|
||||
ProjectPath,
|
||||
};
|
||||
use rpc::proto::{self, update_view, PeerId};
|
||||
use settings::Settings;
|
||||
use workspace::item::{ItemSettings, TabContentParams};
|
||||
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -34,9 +38,9 @@ use text::{BufferId, Selection};
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use ui::{h_flex, prelude::*, Label};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
||||
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
@@ -49,7 +53,6 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<Workspace>,
|
||||
remote_id: ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
@@ -63,7 +66,6 @@ impl FollowableItem for Editor {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let client = project.read(cx).client();
|
||||
let replica_id = project.read(cx).replica_id();
|
||||
let buffer_ids = state
|
||||
.excerpts
|
||||
@@ -77,72 +79,55 @@ impl FollowableItem for Editor {
|
||||
.collect::<Result<Vec<_>>>()
|
||||
});
|
||||
|
||||
let pane = pane.downgrade();
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let mut buffers = futures::future::try_join_all(buffers?)
|
||||
.await
|
||||
.debug_assert_ok("leaders don't share views for unshared buffers")?;
|
||||
|
||||
let editor = pane.update(&mut cx, |pane, cx| {
|
||||
let mut editors = pane.items_of_type::<Self>();
|
||||
editors.find(|editor| {
|
||||
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|
||||
let singleton_buffer_matches = state.singleton
|
||||
&& buffers.first()
|
||||
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
|
||||
ids_match || singleton_buffer_matches
|
||||
let editor = cx.update(|cx| {
|
||||
let multibuffer = cx.new_model(|cx| {
|
||||
let mut multibuffer;
|
||||
if state.singleton && buffers.len() == 1 {
|
||||
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||
} else {
|
||||
multibuffer = MultiBuffer::new(replica_id, project.read(cx).capability());
|
||||
let mut excerpts = state.excerpts.into_iter().peekable();
|
||||
while let Some(excerpt) = excerpts.peek() {
|
||||
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let buffer_excerpts = iter::from_fn(|| {
|
||||
let excerpt = excerpts.peek()?;
|
||||
(excerpt.buffer_id == u64::from(buffer_id))
|
||||
.then(|| excerpts.next().unwrap())
|
||||
});
|
||||
let buffer =
|
||||
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
buffer_excerpts.filter_map(deserialize_excerpt_range),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(title) = &state.title {
|
||||
multibuffer = multibuffer.with_title(title.clone())
|
||||
}
|
||||
|
||||
multibuffer
|
||||
});
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
|
||||
editor.remote_id = Some(remote_id);
|
||||
editor
|
||||
})
|
||||
})?;
|
||||
|
||||
let editor = if let Some(editor) = editor {
|
||||
editor
|
||||
} else {
|
||||
pane.update(&mut cx, |_, cx| {
|
||||
let multibuffer = cx.new_model(|cx| {
|
||||
let mut multibuffer;
|
||||
if state.singleton && buffers.len() == 1 {
|
||||
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||
} else {
|
||||
multibuffer =
|
||||
MultiBuffer::new(replica_id, project.read(cx).capability());
|
||||
let mut excerpts = state.excerpts.into_iter().peekable();
|
||||
while let Some(excerpt) = excerpts.peek() {
|
||||
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let buffer_excerpts = iter::from_fn(|| {
|
||||
let excerpt = excerpts.peek()?;
|
||||
(excerpt.buffer_id == u64::from(buffer_id))
|
||||
.then(|| excerpts.next().unwrap())
|
||||
});
|
||||
let buffer =
|
||||
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
buffer_excerpts.filter_map(deserialize_excerpt_range),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(title) = &state.title {
|
||||
multibuffer = multibuffer.with_title(title.clone())
|
||||
}
|
||||
|
||||
multibuffer
|
||||
});
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
|
||||
editor.remote_id = Some(remote_id);
|
||||
editor
|
||||
})
|
||||
})?
|
||||
};
|
||||
|
||||
update_editor_from_message(
|
||||
editor.downgrade(),
|
||||
project,
|
||||
@@ -327,6 +312,16 @@ impl FollowableItem for Editor {
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
|
||||
let self_singleton = self.buffer.read(cx).as_singleton()?;
|
||||
let other_singleton = existing.buffer.read(cx).as_singleton()?;
|
||||
if self_singleton == other_singleton {
|
||||
Some(Dedup::KeepExisting)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_editor_from_message(
|
||||
@@ -371,7 +366,7 @@ async fn update_editor_from_message(
|
||||
continue;
|
||||
};
|
||||
let buffer_id = BufferId::new(excerpt.buffer_id)?;
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -596,6 +591,20 @@ impl Item for Editor {
|
||||
Some(path.to_string_lossy().to_string().into())
|
||||
}
|
||||
|
||||
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
|
||||
ItemSettings::get_global(cx)
|
||||
.file_icons
|
||||
.then(|| {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
||||
.and_then(|path| FileIcons::get_icon(path.path.as_ref(), cx))
|
||||
})
|
||||
.flatten()
|
||||
.map(|icon| Icon::from_path(icon))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let label_color = if ItemSettings::get_global(cx).git_status {
|
||||
self.buffer()
|
||||
@@ -846,54 +855,8 @@ impl Item for Editor {
|
||||
Some(breadcrumbs)
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, _: &mut ViewContext<Self>) {
|
||||
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
|
||||
let Some(workspace_id) = workspace.database_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let item_id = cx.view().item_id().as_u64() as ItemId;
|
||||
|
||||
fn serialize(
|
||||
buffer: Model<Buffer>,
|
||||
workspace_id: WorkspaceId,
|
||||
item_id: ItemId,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
||||
let path = file.abs_path(cx);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
DB.save_path(item_id, workspace_id, path.clone())
|
||||
.await
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
serialize(buffer.clone(), workspace_id, item_id, cx);
|
||||
|
||||
cx.subscribe(&buffer, |this, buffer, event, cx| {
|
||||
if let Some((_, Some(workspace_id))) = this.workspace.as_ref() {
|
||||
if let language::Event::FileHandleChanged = event {
|
||||
serialize(
|
||||
buffer,
|
||||
*workspace_id,
|
||||
cx.view().item_id().as_u64() as ItemId,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("Editor")
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
|
||||
@@ -929,6 +892,20 @@ impl Item for Editor {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableItem for Editor {
|
||||
fn serialized_item_kind() -> &'static str {
|
||||
"Editor"
|
||||
}
|
||||
|
||||
fn cleanup(
|
||||
workspace_id: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(|_| DB.delete_unloaded_items(workspace_id, alive_items))
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: Model<Project>,
|
||||
@@ -937,41 +914,171 @@ impl Item for Editor {
|
||||
item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let project_item: Result<_> = project.update(cx, |project, cx| {
|
||||
// Look up the path with this key associated, create a self with that path
|
||||
let path = DB
|
||||
.get_path(item_id, workspace_id)?
|
||||
.context("No path stored for this editor")?;
|
||||
let path_content_language = match DB
|
||||
.get_path_and_contents(item_id, workspace_id)
|
||||
.context("Failed to query editor state")
|
||||
{
|
||||
Ok(Some((path, content, language))) => {
|
||||
if ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers
|
||||
{
|
||||
(path, content, language)
|
||||
} else {
|
||||
(path, None, None)
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
return Task::ready(Err(anyhow!("No path or contents found for buffer")));
|
||||
}
|
||||
Err(error) => {
|
||||
return Task::ready(Err(error));
|
||||
}
|
||||
};
|
||||
|
||||
let (worktree, path) = project
|
||||
.find_local_worktree(&path, cx)
|
||||
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: path.into(),
|
||||
};
|
||||
match path_content_language {
|
||||
(None, Some(content), language_name) => cx.spawn(|_, mut cx| async move {
|
||||
let language = if let Some(language_name) = language_name {
|
||||
let language_registry =
|
||||
project.update(&mut cx, |project, _| project.languages().clone())?;
|
||||
|
||||
Ok(project.open_path(project_path, cx))
|
||||
});
|
||||
Some(language_registry.language_for_name(&language_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
project_item
|
||||
.map(|project_item| {
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
let (_, project_item) = project_item.await?;
|
||||
let buffer = project_item
|
||||
.downcast::<Buffer>()
|
||||
.map_err(|_| anyhow!("Project item at stored path was not a buffer"))?;
|
||||
pane.update(&mut cx, |_, cx| {
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
// First create the empty buffer
|
||||
let buffer = project.update(&mut cx, |project, cx| {
|
||||
project.create_local_buffer("", language, cx)
|
||||
})?;
|
||||
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
// Then set the text so that the dirty bit is set correctly
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_text(content, cx);
|
||||
})?;
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
})
|
||||
}),
|
||||
(Some(path), contents, _) => {
|
||||
let project_item = project.update(cx, |project, cx| {
|
||||
let (worktree, path) = project
|
||||
.find_worktree(&path, cx)
|
||||
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: path.into(),
|
||||
};
|
||||
|
||||
Ok(project.open_path(project_path, cx))
|
||||
});
|
||||
|
||||
project_item
|
||||
.map(|project_item| {
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
let (_, project_item) = project_item.await?;
|
||||
let buffer = project_item.downcast::<Buffer>().map_err(|_| {
|
||||
anyhow!("Project item at stored path was not a buffer")
|
||||
})?;
|
||||
|
||||
// This is a bit wasteful: we're loading the whole buffer from
|
||||
// disk and then overwrite the content.
|
||||
// But for now, it keeps the implementation of the content serialization
|
||||
// simple, because we don't have to persist all of the metadata that we get
|
||||
// by loading the file (git diff base, mtime, ...).
|
||||
if let Some(buffer_text) = contents {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_text(buffer_text, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
pane.update(&mut cx, |_, cx| {
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
_ => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
item_id: ItemId,
|
||||
closing: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let mut serialize_dirty_buffers = self.serialize_dirty_buffers;
|
||||
|
||||
let project = self.project.clone()?;
|
||||
if project.read(cx).visible_worktrees(cx).next().is_none() {
|
||||
// If we don't have a worktree, we don't serialize, because
|
||||
// projects without worktrees aren't deserialized.
|
||||
serialize_dirty_buffers = false;
|
||||
}
|
||||
|
||||
if closing && !serialize_dirty_buffers {
|
||||
return None;
|
||||
}
|
||||
|
||||
let workspace_id = workspace.database_id()?;
|
||||
|
||||
let buffer = self.buffer().read(cx).as_singleton()?;
|
||||
|
||||
let is_dirty = buffer.read(cx).is_dirty();
|
||||
let path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| file.as_local())
|
||||
.map(|file| file.abs_path(cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
Some(cx.spawn(|_this, cx| async move {
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
if let Some(path) = path {
|
||||
DB.save_path(item_id, workspace_id, path.clone())
|
||||
.await
|
||||
.context("failed to save path of buffer")?
|
||||
}
|
||||
|
||||
if serialize_dirty_buffers {
|
||||
let (contents, language) = if is_dirty {
|
||||
let contents = snapshot.text();
|
||||
let language = snapshot.language().map(|lang| lang.name().to_string());
|
||||
(Some(contents), language)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
DB.save_contents(item_id, workspace_id, contents, language)
|
||||
.await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
.await
|
||||
.context("failed to save contents of buffer")?;
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn should_serialize(&self, event: &Self::Event) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,62 @@ use gpui::prelude::FluentBuilder;
|
||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||
use workspace::OpenInTerminal;
|
||||
|
||||
pub enum MenuPosition {
|
||||
/// When the editor is scrolled, the context menu stays on the exact
|
||||
/// same position on the screen, never disappearing.
|
||||
PinnedToScreen(Point<Pixels>),
|
||||
/// When the editor is scrolled, the context menu follows the position it is associated with.
|
||||
/// Disappears when the position is no longer visible.
|
||||
PinnedToEditor {
|
||||
source: multi_buffer::Anchor,
|
||||
offset_x: Pixels,
|
||||
offset_y: Pixels,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct MouseContextMenu {
|
||||
pub(crate) position: Point<Pixels>,
|
||||
pub(crate) position: MenuPosition,
|
||||
pub(crate) context_menu: View<ui::ContextMenu>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl MouseContextMenu {
|
||||
pub(crate) fn new(
|
||||
pub(crate) fn pinned_to_editor(
|
||||
editor: &mut Editor,
|
||||
source: multi_buffer::Anchor,
|
||||
position: Point<Pixels>,
|
||||
context_menu: View<ui::ContextMenu>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<Self> {
|
||||
let context_menu_focus = context_menu.focus_handle(cx);
|
||||
cx.focus(&context_menu_focus);
|
||||
|
||||
let _subscription = cx.subscribe(
|
||||
&context_menu,
|
||||
move |editor, _, _event: &DismissEvent, cx| {
|
||||
editor.mouse_context_menu.take();
|
||||
if context_menu_focus.contains_focused(cx) {
|
||||
editor.focus(cx);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let editor_snapshot = editor.snapshot(cx);
|
||||
let source_point = editor.to_pixel_point(source, &editor_snapshot, cx)?;
|
||||
let offset = position - source_point;
|
||||
|
||||
Some(Self {
|
||||
position: MenuPosition::PinnedToEditor {
|
||||
source,
|
||||
offset_x: offset.x,
|
||||
offset_y: offset.y,
|
||||
},
|
||||
context_menu,
|
||||
_subscription,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn pinned_to_screen(
|
||||
position: Point<Pixels>,
|
||||
context_menu: View<ui::ContextMenu>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
@@ -25,16 +73,18 @@ impl MouseContextMenu {
|
||||
let context_menu_focus = context_menu.focus_handle(cx);
|
||||
cx.focus(&context_menu_focus);
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| {
|
||||
this.mouse_context_menu.take();
|
||||
let _subscription = cx.subscribe(
|
||||
&context_menu,
|
||||
move |editor, _, _event: &DismissEvent, cx| {
|
||||
editor.mouse_context_menu.take();
|
||||
if context_menu_focus.contains_focused(cx) {
|
||||
this.focus(cx);
|
||||
editor.focus(cx);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
position,
|
||||
position: MenuPosition::PinnedToScreen(position),
|
||||
context_menu,
|
||||
_subscription,
|
||||
}
|
||||
@@ -49,13 +99,11 @@ fn display_ranges<'a>(
|
||||
.pending
|
||||
.as_ref()
|
||||
.map(|pending| &pending.selection);
|
||||
selections.disjoint.iter().chain(pending).map(move |s| {
|
||||
if s.reversed {
|
||||
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
|
||||
} else {
|
||||
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
|
||||
}
|
||||
})
|
||||
selections
|
||||
.disjoint
|
||||
.iter()
|
||||
.chain(pending)
|
||||
.map(move |s| s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map))
|
||||
}
|
||||
|
||||
pub fn deploy_context_menu(
|
||||
@@ -73,13 +121,15 @@ pub fn deploy_context_menu(
|
||||
return;
|
||||
}
|
||||
|
||||
let display_map = editor.selections.display_map(cx);
|
||||
let source_anchor = display_map.display_point_to_anchor(point, text::Bias::Right);
|
||||
let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
|
||||
let menu = custom(editor, point, cx);
|
||||
editor.custom_context_menu = Some(custom);
|
||||
if menu.is_none() {
|
||||
let Some(menu) = menu else {
|
||||
return;
|
||||
}
|
||||
menu.unwrap()
|
||||
};
|
||||
menu
|
||||
} else {
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
if editor.project.is_none() {
|
||||
@@ -87,17 +137,20 @@ pub fn deploy_context_menu(
|
||||
}
|
||||
|
||||
let display_map = editor.selections.display_map(cx);
|
||||
let buffer = &editor.snapshot(cx).buffer_snapshot;
|
||||
let anchor = buffer.anchor_before(point.to_point(&display_map));
|
||||
if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) {
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
s.set_pending_anchor_range(anchor..anchor, SelectMode::Character);
|
||||
});
|
||||
}
|
||||
|
||||
let focus = cx.focused();
|
||||
ui::ContextMenu::build(cx, |menu, _cx| {
|
||||
let builder = menu
|
||||
.on_blur_subscription(Subscription::new(|| {}))
|
||||
.action("Rename Symbol", Box::new(Rename))
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||
@@ -128,8 +181,9 @@ pub fn deploy_context_menu(
|
||||
}
|
||||
})
|
||||
};
|
||||
let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx);
|
||||
editor.mouse_context_menu = Some(mouse_context_menu);
|
||||
|
||||
editor.mouse_context_menu =
|
||||
MouseContextMenu::pinned_to_editor(editor, source_anchor, position, context_menu, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use db::sqlez::statement::Statement;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
@@ -10,10 +12,12 @@ define_connection!(
|
||||
// editors(
|
||||
// item_id: usize,
|
||||
// workspace_id: usize,
|
||||
// path: PathBuf,
|
||||
// path: Option<PathBuf>,
|
||||
// scroll_top_row: usize,
|
||||
// scroll_vertical_offset: f32,
|
||||
// scroll_horizontal_offset: f32,
|
||||
// content: Option<String>,
|
||||
// language: Option<String>,
|
||||
// )
|
||||
pub static ref DB: EditorDb<WorkspaceDb> =
|
||||
&[sql! (
|
||||
@@ -31,13 +35,39 @@ define_connection!(
|
||||
ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
|
||||
ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
|
||||
),
|
||||
sql! (
|
||||
// Since sqlite3 doesn't support ALTER COLUMN, we create a new
|
||||
// table, move the data over, drop the old table, rename new table.
|
||||
CREATE TABLE new_editors_tmp (
|
||||
item_id INTEGER NOT NULL,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
path BLOB, // <-- No longer "NOT NULL"
|
||||
scroll_top_row INTEGER NOT NULL DEFAULT 0,
|
||||
scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
|
||||
scroll_vertical_offset REAL NOT NULL DEFAULT 0,
|
||||
contents TEXT, // New
|
||||
language TEXT, // New
|
||||
PRIMARY KEY(item_id, workspace_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
|
||||
SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
|
||||
FROM editors;
|
||||
|
||||
DROP TABLE editors;
|
||||
|
||||
ALTER TABLE new_editors_tmp RENAME TO editors;
|
||||
)];
|
||||
);
|
||||
|
||||
impl EditorDb {
|
||||
query! {
|
||||
pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
|
||||
SELECT path FROM editors
|
||||
pub fn get_path_and_contents(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(Option<PathBuf>, Option<String>, Option<String>)>> {
|
||||
SELECT path, contents, language FROM editors
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
@@ -55,6 +85,20 @@ impl EditorDb {
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: Option<String>, language: Option<String>) -> Result<()> {
|
||||
INSERT INTO editors
|
||||
(item_id, workspace_id, contents, language)
|
||||
VALUES
|
||||
(?1, ?2, ?3, ?4)
|
||||
ON CONFLICT DO UPDATE SET
|
||||
item_id = ?1,
|
||||
workspace_id = ?2,
|
||||
contents = ?3,
|
||||
language = ?4
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the scroll top row, and offset
|
||||
query! {
|
||||
pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
|
||||
@@ -80,4 +124,75 @@ impl EditorDb {
|
||||
WHERE item_id = ?1 AND workspace_id = ?2
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_unloaded_items(
|
||||
&self,
|
||||
workspace: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
) -> Result<()> {
|
||||
let placeholders = alive_items
|
||||
.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ");
|
||||
|
||||
let query = format!(
|
||||
"DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
|
||||
);
|
||||
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&workspace, 1)?;
|
||||
for id in alive_items {
|
||||
next_index = statement.bind(&id, next_index)?;
|
||||
}
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_saving_content() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
|
||||
// Sanity check: make sure there is no row in the `editors` table
|
||||
assert_eq!(DB.get_path_and_contents(1234, workspace_id).unwrap(), None);
|
||||
|
||||
// Save content/language
|
||||
DB.save_contents(
|
||||
1234,
|
||||
workspace_id,
|
||||
Some("testing".into()),
|
||||
Some("Go".into()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check that it can be read from DB
|
||||
let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
|
||||
let (path, contents, language) = path_and_contents.unwrap();
|
||||
assert!(path.is_none());
|
||||
assert_eq!(contents, Some("testing".to_owned()));
|
||||
assert_eq!(language, Some("Go".to_owned()));
|
||||
|
||||
// Update it with NULL
|
||||
DB.save_contents(1234, workspace_id, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check that it worked
|
||||
let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
|
||||
let (path, contents, language) = path_and_contents.unwrap();
|
||||
assert!(path.is_none());
|
||||
assert!(contents.is_none());
|
||||
assert!(language.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ pub fn expand_macro_recursively(
|
||||
cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx)),
|
||||
),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
|
||||
@@ -476,7 +476,10 @@ impl Editor {
|
||||
}
|
||||
|
||||
let cur_position = self.scroll_position(cx);
|
||||
let new_pos = cur_position + point(0., amount.lines(self));
|
||||
let Some(visible_line_count) = self.visible_line_count() else {
|
||||
return;
|
||||
};
|
||||
let new_pos = cur_position + point(0., amount.lines(visible_line_count));
|
||||
self.set_scroll_position(new_pos, cx);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user