Compare commits
132 Commits
v0.146.4
...
tool-calli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfa12d92ef | ||
|
|
f6b8fad275 | ||
|
|
14b26034d8 | ||
|
|
70e895a8c7 | ||
|
|
4bd935b409 | ||
|
|
c0df1e1846 | ||
|
|
e9d0768e3c | ||
|
|
380a99038b | ||
|
|
88653c4e3e | ||
|
|
3751f67730 | ||
|
|
6af385c09e | ||
|
|
e6cd1cf22b | ||
|
|
a1bd7a1297 | ||
|
|
3e31955b7f | ||
|
|
be86852f95 | ||
|
|
bde02a350e | ||
|
|
4c9311ba40 | ||
|
|
c8bc49fa18 | ||
|
|
bcd972fbb4 | ||
|
|
e423f03ba6 | ||
|
|
03ebbcbef6 | ||
|
|
27f97ba762 | ||
|
|
769ae8b101 | ||
|
|
d27fef7b2c | ||
|
|
f4bbbe69b4 | ||
|
|
c937a2fcdd | ||
|
|
a5279cc48a | ||
|
|
4d56252bae | ||
|
|
0360cda543 | ||
|
|
5e04753d1c | ||
|
|
71312e5692 | ||
|
|
05825e9804 | ||
|
|
73d682c010 | ||
|
|
e59e47fe7f | ||
|
|
4abf7f058e | ||
|
|
f980e40993 | ||
|
|
57b2cb6f60 | ||
|
|
af014a2530 | ||
|
|
243fb3562c | ||
|
|
e830865eb1 | ||
|
|
7aa6f4788d | ||
|
|
18daf17d0e | ||
|
|
856d9632e4 | ||
|
|
745d2e4d3b | ||
|
|
50dbab0747 | ||
|
|
70c22cbdd6 | ||
|
|
9621005851 | ||
|
|
05003ed4c5 | ||
|
|
2c610c0e57 | ||
|
|
479ffbbd51 | ||
|
|
fe23504eba | ||
|
|
95d82f88de | ||
|
|
4000b0a02c | ||
|
|
02c43a5bf2 | ||
|
|
f2060ccbe0 | ||
|
|
13693ff80f | ||
|
|
ec5886a078 | ||
|
|
10c9e337cf | ||
|
|
1da6a12bb4 | ||
|
|
cc1d3f0a35 | ||
|
|
22118f15e9 | ||
|
|
0d5de88c4b | ||
|
|
f291677d40 | ||
|
|
9d736fe80c | ||
|
|
f3ad754396 | ||
|
|
86456ce379 | ||
|
|
d755d29577 | ||
|
|
ab3c9f0678 | ||
|
|
201db23b58 | ||
|
|
beb8fbdf7f | ||
|
|
d2501e8886 | ||
|
|
82d6ad4616 | ||
|
|
a60b3b9389 | ||
|
|
06863144c6 | ||
|
|
b7c6f3e98e | ||
|
|
7146087b44 | ||
|
|
6d3eaa055f | ||
|
|
f31c55a76f | ||
|
|
97750529fe | ||
|
|
cd9dd5ccf7 | ||
|
|
3ce864e69e | ||
|
|
9eeb564c5c | ||
|
|
847bd35bd9 | ||
|
|
b8e5ddf456 | ||
|
|
6998c03c59 | ||
|
|
8631180e43 | ||
|
|
cd9a42e8da | ||
|
|
3246a932ca | ||
|
|
8ba392bba6 | ||
|
|
856a8ef5e8 | ||
|
|
6dd9ce1376 | ||
|
|
fbbea7ab01 | ||
|
|
aded3dfb05 | ||
|
|
3053f98652 | ||
|
|
ebd407deb6 | ||
|
|
5e44f5fa44 | ||
|
|
a07122d78f | ||
|
|
6a079cbdc3 | ||
|
|
c7e2d5bd89 | ||
|
|
6bff8ecb73 | ||
|
|
e2113e4895 | ||
|
|
b56e4ff2af | ||
|
|
88f68101d4 | ||
|
|
d852a32ef1 | ||
|
|
b53c3b84e2 | ||
|
|
3d6b07d78c | ||
|
|
4c0d0ad4f4 | ||
|
|
55575ca830 | ||
|
|
ea44af4a85 | ||
|
|
5865c5e80f | ||
|
|
51f8013616 | ||
|
|
b9570218b6 | ||
|
|
fb4d77c008 | ||
|
|
b14bb6bda4 | ||
|
|
8501ae6a19 | ||
|
|
4e88a08ed4 | ||
|
|
659f34bf21 | ||
|
|
001376fd6d | ||
|
|
298ca5ff1b | ||
|
|
596ee58be8 | ||
|
|
fd4a4127eb | ||
|
|
0297a42735 | ||
|
|
9c9a0bd24f | ||
|
|
740c444089 | ||
|
|
325e6b9fef | ||
|
|
65c63defcc | ||
|
|
274e56b086 | ||
|
|
7fb906d774 | ||
|
|
d107d22c2d | ||
|
|
23dac9cfce | ||
|
|
777ddefa73 | ||
|
|
77a2f2490e |
@@ -12,3 +12,7 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
# This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = ["--cfg", "windows_slim_errors"]
|
||||
|
||||
2
.github/actions/run_tests/action.yml
vendored
2
.github/actions/run_tests/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}
|
||||
|
||||
41
.github/workflows/ci.yml
vendored
41
.github/workflows/ci.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -137,17 +137,18 @@ jobs:
|
||||
runs-on: hosted-windows-1
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: cargo clippy
|
||||
run: ./script/clippy
|
||||
# Windows can't run shell scripts, so we need to use `cargo xtask`.
|
||||
run: cargo xtask clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
@@ -170,12 +171,12 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
# We need to fetch more than one commit so that `script/draft-release-notes`
|
||||
# is able to diff between the current and previous tag.
|
||||
@@ -230,26 +231,26 @@ jobs:
|
||||
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- name: Upload app bundle (universal) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
|
||||
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
|
||||
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
|
||||
path: target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
@@ -280,7 +281,7 @@ jobs:
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -318,14 +319,14 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
@@ -347,11 +348,11 @@ jobs:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
- name: "Setup jq"
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
uses: dcarbone/install-jq-action@8867ddb4788346d7c22b72ea2e2ffe4d514c7bcb # v2
|
||||
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
@@ -359,7 +360,7 @@ jobs:
|
||||
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
|
||||
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
@@ -402,14 +403,14 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
|
||||
4
.github/workflows/danger.yml
vendored
4
.github/workflows/danger.yml
vendored
@@ -14,14 +14,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
12
.github/workflows/deploy_cloudflare.yml
vendored
12
.github/workflows/deploy_cloudflare.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
|
||||
with:
|
||||
mdbook-version: "0.4.37"
|
||||
|
||||
@@ -28,28 +28,28 @@ jobs:
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy Docs
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
|
||||
- name: Deploy Install
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
|
||||
|
||||
- name: Deploy Docs Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||
|
||||
- name: Deploy Install Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
|
||||
6
.github/workflows/deploy_collab.yml
vendored
6
.github/workflows/deploy_collab.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
needs: style
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
run: doctl registry login
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
|
||||
4
.github/workflows/publish_extension_cli.yml
vendored
4
.github/workflows/publish_extension_cli.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
|
||||
4
.github/workflows/randomized_tests.yml
vendored
4
.github/workflows/randomized_tests.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
- randomized-tests
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
|
||||
4
.github/workflows/release_actions.yml
vendored
4
.github/workflows/release_actions.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
fi
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
uses: 2428392/gh-truncate-string-action@67b1b814955634208b103cff064be3cb1c7a19be # v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
maxLength: 2000
|
||||
truncationSymbol: "..."
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
16
.github/workflows/release_nightly.yml
vendored
16
.github/workflows/release_nightly.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
needs: style
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -69,12 +69,12 @@ jobs:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -141,12 +141,12 @@ jobs:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: "Setup jq"
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
uses: dcarbone/install-jq-action@8867ddb4788346d7c22b72ea2e2ffe4d514c7bcb # v2
|
||||
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
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
|
||||
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: "x64"
|
||||
|
||||
@@ -8,8 +8,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: "x64"
|
||||
|
||||
1282
Cargo.lock
generated
1282
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
112
Cargo.toml
112
Cargo.toml
@@ -1,11 +1,11 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_tooling",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -125,6 +125,10 @@ members = [
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
|
||||
#
|
||||
# Extensions
|
||||
#
|
||||
|
||||
"extensions/astro",
|
||||
"extensions/clojure",
|
||||
"extensions/csharp",
|
||||
@@ -154,20 +158,25 @@ members = [
|
||||
"extensions/vue",
|
||||
"extensions/zig",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
#
|
||||
|
||||
"tooling/xtask",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
#
|
||||
# Workspace member crates
|
||||
#
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
aho-corasick = "1.1"
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
@@ -240,6 +249,7 @@ project_symbols = { path = "crates/project_symbols" }
|
||||
proto = { path = "crates/proto" }
|
||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
remote = { path = "crates/remote" }
|
||||
remote_server = { path = "crates/remote_server" }
|
||||
@@ -285,39 +295,44 @@ worktree = { path = "crates/worktree" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
#
|
||||
# External crates
|
||||
#
|
||||
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.23"
|
||||
any_vec = "0.13"
|
||||
anyhow = "1.0.57"
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
ashpd = "0.9.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = { version = "0.1" }
|
||||
async-dispatcher = "0.1"
|
||||
async-fs = "1.6"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = { version = "0.16" }
|
||||
async-tungstenite = "0.23"
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.13"
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
blade-util = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
cap-std = "3.0"
|
||||
cargo_metadata = "0.18"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = { version = "0.11.6" }
|
||||
clickhouse = "0.11.6"
|
||||
cocoa = "0.25"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation = "0.9.3"
|
||||
core-foundation-sys = "0.8.6"
|
||||
ctor = "0.2.6"
|
||||
dashmap = "5.5.3"
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.10"
|
||||
env_logger = "0.11"
|
||||
exec = "0.3.1"
|
||||
fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
@@ -331,12 +346,13 @@ html5ever = "0.27.0"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "1"
|
||||
indoc = "2"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
] }
|
||||
itertools = "0.11.0"
|
||||
jsonwebtoken = "9.3"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
@@ -358,14 +374,14 @@ prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.10.0", default-features = false }
|
||||
rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.14", default-features = false, features = [
|
||||
runtimelib = { version = "0.12", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
schemars = {version = "0.8", features = ["impl_json_schema"]}
|
||||
semver = "1.0"
|
||||
@@ -387,6 +403,7 @@ smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.30.7"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.29"
|
||||
@@ -402,29 +419,28 @@ tiny_http = "0.8"
|
||||
toml = "0.8"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.20", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.20.5"
|
||||
tree-sitter-c = "0.20.1"
|
||||
tree-sitter-cpp = "0.20.5"
|
||||
tree-sitter-css = "0.20"
|
||||
tree-sitter-elixir = "0.1.1"
|
||||
tree-sitter = { version = "0.22", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.21"
|
||||
tree-sitter-c = "0.21"
|
||||
tree-sitter-cpp = "0.22"
|
||||
tree-sitter-css = "0.21"
|
||||
tree-sitter-elixir = "0.2"
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "b82ab803d887002a0af11f6ce63d72884580bf33" }
|
||||
tree-sitter-gomod = "1.0.1"
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
rustc-demangle = "0.1.23"
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-json = "0.20.2"
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-regex = "0.20.0"
|
||||
tree-sitter-ruby = "0.20.0"
|
||||
tree-sitter-rust = "0.20.3"
|
||||
tree-sitter-typescript = "0.20.5"
|
||||
tree-sitter-yaml = "0.0.1"
|
||||
tree-sitter-go = "0.21"
|
||||
tree-sitter-go-mod = { git = "https://github.com/SomeoneToIgnore/tree-sitter-go-mod", rev = "8c1f54f12bb4c846336b634bc817645d6f35d641", package = "tree-sitter-gomod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work", rev = "dcbabff454703c3a4bc98a23cf8778d4be46fd22" }
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "6dd0303acf7138dd2b9b432a229e16539581c701" }
|
||||
tree-sitter-html = "0.20"
|
||||
tree-sitter-jsdoc = "0.21"
|
||||
tree-sitter-json = "0.21"
|
||||
tree-sitter-md = { git = "https://github.com/zed-industries/tree-sitter-markdown", rev = "e3855e37f8f2c71aa7513c18a9c95fb7461b1b10" }
|
||||
protols-tree-sitter-proto = "0.2"
|
||||
tree-sitter-python = "0.21"
|
||||
tree-sitter-regex = "0.21"
|
||||
tree-sitter-ruby = "0.21"
|
||||
tree-sitter-rust = "0.21"
|
||||
tree-sitter-typescript = "0.21"
|
||||
tree-sitter-yaml = "0.6"
|
||||
unindent = "0.1.7"
|
||||
unicase = "2.6"
|
||||
unicode-segmentation = "1.10"
|
||||
@@ -432,20 +448,19 @@ url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
|
||||
wasmparser = "0.201"
|
||||
wasm-encoder = "0.201"
|
||||
wasmtime = { version = "19.0.2", default-features = false, features = [
|
||||
wasmtime = { version = "21.0.1", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
] }
|
||||
wasmtime-wasi = "19.0.2"
|
||||
wasmtime-wasi = "21.0.1"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.57"
|
||||
version = "0.58"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
@@ -465,7 +480,6 @@ features = [
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_DataExchange",
|
||||
@@ -485,7 +499,8 @@ features = [
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7b4894ba2ae81b988846676f54c0988d4027ef4f" }
|
||||
# Patch Tree-sitter for updated wasmtime.
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7f4a57817d58a2f134fe863674acad6bbf007228" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
@@ -538,13 +553,6 @@ single_range_in_vec_init = "allow"
|
||||
style = { level = "allow", priority = -1 }
|
||||
|
||||
# Individual rules that have violations in the codebase:
|
||||
almost_complete_range = "allow"
|
||||
arc_with_non_send_sync = "allow"
|
||||
borrowed_box = "allow"
|
||||
let_underscore_future = "allow"
|
||||
map_entry = "allow"
|
||||
non_canonical_partial_ord_impl = "allow"
|
||||
reversed_empty_ranges = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.79-bookworm as builder
|
||||
FROM rust:1.80-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
1
assets/icons/eye.svg
Normal file
1
assets/icons/eye.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 358 B |
1
assets/icons/file_code.svg
Normal file
1
assets/icons/file_code.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-code"><path d="M10 12.5 8 15l2 2.5"/><path d="m14 12.5 2 2.5-2 2.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/></svg>
|
||||
|
After Width: | Height: | Size: 388 B |
1
assets/icons/file_text.svg
Normal file
1
assets/icons/file_text.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-text"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
|
||||
|
After Width: | Height: | Size: 384 B |
6
assets/icons/sliders-alt.svg
Normal file
6
assets/icons/sliders-alt.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4H8" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 10L11 10" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.75"/>
|
||||
<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 450 B |
@@ -105,6 +105,7 @@
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"ctrl-enter": "editor::NewlineAbove",
|
||||
"ctrl-shift-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
@@ -115,12 +116,6 @@
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && !jupyter",
|
||||
"bindings": {
|
||||
"ctrl-enter": "editor::NewlineAbove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && inline_completion",
|
||||
"bindings": {
|
||||
@@ -249,6 +244,13 @@
|
||||
"ctrl-alt-shift-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"]
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -458,16 +460,12 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
||||
// TODO: Move this to a dock open action
|
||||
"ctrl-shift-c": "collab_panel::ToggleFocus",
|
||||
"ctrl-alt-i": "zed::DebugElements",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "!Terminal",
|
||||
"bindings": {
|
||||
"ctrl-shift-c": "collab_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
@@ -601,14 +599,12 @@
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"shift-ctrl-c": "terminal::Copy",
|
||||
"ctrl-insert": "terminal::Copy",
|
||||
// "ctrl-a": "editor::SelectAll", // conflicts with readline
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"shift-ctrl-v": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||
"down": ["terminal::SendKeystroke", "down"],
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-shift-enter": "editor::NewlineAbove",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"cmd-f": "buffer_search::Deploy",
|
||||
@@ -146,12 +147,6 @@
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && !jupyter",
|
||||
"bindings": {
|
||||
"cmd-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && inline_completion",
|
||||
"bindings": {
|
||||
@@ -298,7 +293,6 @@
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
"alt-cmd-f": "project_search::ToggleFilters",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
"alt-cmd-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Plex Mono",
|
||||
// Set the buffer text's font fallbacks, this will be merged with
|
||||
// the platform's default fallbacks.
|
||||
"buffer_font_fallbacks": [],
|
||||
// The OpenType features to enable for text in the editor.
|
||||
"buffer_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -47,8 +50,11 @@
|
||||
// },
|
||||
"buffer_line_height": "comfortable",
|
||||
// The name of a font to use for rendering text in the UI
|
||||
// (On macOS) You can set this to ".SystemUIFont" to use the system font
|
||||
// You can set this to ".SystemUIFont" to use the system font
|
||||
"ui_font_family": "Zed Plex Sans",
|
||||
// Set the UI's font fallbacks, this will be merged with the platform's
|
||||
// default font fallbacks.
|
||||
"ui_font_fallbacks": [],
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -312,7 +318,7 @@
|
||||
"auto_reveal_entries": true,
|
||||
// Whether to fold directories automatically and show compact folders
|
||||
// (e.g. "a/b/c" ) when a directory has only one subdirectory inside.
|
||||
"auto_fold_dirs": false,
|
||||
"auto_fold_dirs": true,
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
@@ -675,6 +681,10 @@
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Plex Mono",
|
||||
// Set the terminal's font fallbacks. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font fallbacks.
|
||||
// This will be merged with the platform's default font fallbacks
|
||||
// "font_fallbacks": ["FiraCode Nerd Fonts"],
|
||||
// Sets the maximum number of lines in the terminal's scrollback buffer.
|
||||
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
@@ -965,5 +975,21 @@
|
||||
// {
|
||||
// "W": "workspace::Save"
|
||||
// }
|
||||
"command_aliases": {}
|
||||
"command_aliases": {},
|
||||
// ssh_connections is an array of ssh connections.
|
||||
// By default this setting is null, which disables the direct ssh connection support.
|
||||
// You can configure these from `project: Open Remote` in the command palette.
|
||||
// Zed's ssh support will pull configuration from your ~/.ssh too.
|
||||
// Examples:
|
||||
// [
|
||||
// {
|
||||
// "host": "example-box",
|
||||
// "projects": [
|
||||
// {
|
||||
// "paths": ["/home/user/code/zed"]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
"ssh_connections": null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{}
|
||||
|
||||
@@ -100,21 +100,13 @@ impl From<Role> for String {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Request {
|
||||
#[serde(serialize_with = "serialize_request_model")]
|
||||
pub model: Model,
|
||||
pub model: String,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
pub system: String,
|
||||
pub max_tokens: u32,
|
||||
}
|
||||
|
||||
fn serialize_request_model<S>(model: &Model, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&model.id())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct RequestMessage {
|
||||
pub role: Role,
|
||||
|
||||
@@ -38,7 +38,7 @@ Considering these aspects will ensure our conversation view design is optimized
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multiple people, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
|
||||
@@ -532,7 +532,21 @@ impl EditOperation {
|
||||
.path_candidates
|
||||
.iter()
|
||||
.find(|item| item.string == symbol)
|
||||
.context("symbol not found")?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"symbol {:?} not found in path {:?}.\ncandidates: {:?}.\nparse status: {:?}. text:\n{}",
|
||||
symbol,
|
||||
path,
|
||||
outline
|
||||
.path_candidates
|
||||
.iter()
|
||||
.map(|candidate| &candidate.string)
|
||||
.collect::<Vec<_>>(),
|
||||
*parse_status.borrow(),
|
||||
buffer.read_with(&cx, |buffer, _| buffer.text()).unwrap_or_else(|_| "error".to_string())
|
||||
)
|
||||
})?;
|
||||
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
let outline_item = &outline.items[candidate.id];
|
||||
let symbol_range = outline_item.range.to_point(buffer);
|
||||
@@ -1123,16 +1137,17 @@ impl Context {
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
if let Some(token_count) = cx.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})? {
|
||||
let token_count = token_count.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify()
|
||||
})?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify()
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
|
||||
@@ -1420,27 +1420,34 @@ impl Render for PromptEditor {
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
IconButton::new("context", IconName::SlidersAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.with_info_text(
|
||||
"Inline edits use context\n\
|
||||
from the currently selected\n\
|
||||
assistant panel tab.",
|
||||
),
|
||||
)
|
||||
.children(
|
||||
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
@@ -1628,15 +1635,18 @@ impl PromptEditor {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
if let Some(token_count) = cx.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})? {
|
||||
let token_count = token_count.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1825,6 +1835,7 @@ impl PromptEditor {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::{assistant_settings::AssistantSettings, LanguageModelCompletionProvider};
|
||||
use fs::Fs;
|
||||
use gpui::SharedString;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
@@ -11,6 +12,7 @@ pub struct ModelSelector<T: PopoverTrigger> {
|
||||
handle: Option<PopoverMenuHandle<ContextMenu>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
trigger: T,
|
||||
info_text: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
@@ -19,6 +21,7 @@ impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
handle: None,
|
||||
fs,
|
||||
trigger,
|
||||
info_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +29,11 @@ impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.info_text = Some(text.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
|
||||
@@ -35,8 +43,20 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
|
||||
menu = menu.with_handle(handle);
|
||||
}
|
||||
|
||||
let info_text = self.info_text.clone();
|
||||
|
||||
menu.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
if let Some(info_text) = info_text.clone() {
|
||||
menu = menu
|
||||
.custom_row(move |_cx| {
|
||||
Label::new(info_text.clone())
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
|
||||
for (index, provider) in LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
|
||||
@@ -734,26 +734,29 @@ impl PromptLibrary {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(
|
||||
LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: body.to_string(),
|
||||
}],
|
||||
stop: Vec::new(),
|
||||
temperature: 1.,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
prompt_editor.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
if let Some(token_count) = cx.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(
|
||||
LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: body.to_string(),
|
||||
}],
|
||||
stop: Vec::new(),
|
||||
temperature: 1.,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})? {
|
||||
let token_count = token_count.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
prompt_editor.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ impl DiagnosticsSlashCommand {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<str> = "".into();
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
|
||||
@@ -219,7 +219,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
if index {
|
||||
// We don't need to hold onto this task, as the `IndexedDocsStore` will hold it
|
||||
// until it completes.
|
||||
let _ = store.clone().index(package.as_str().into());
|
||||
drop(store.clone().index(package.as_str().into()));
|
||||
}
|
||||
|
||||
let items = store.search(package).await;
|
||||
|
||||
@@ -29,7 +29,7 @@ impl FileSlashCommand {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<str> = "".into();
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
|
||||
@@ -707,15 +707,18 @@ impl PromptEditor {
|
||||
inline_assistant.request_for_inline_assist(assist_id, cx)
|
||||
})??;
|
||||
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
if let Some(token_count) = cx.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})? {
|
||||
let token_count = token_count.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -906,6 +909,7 @@ impl PromptEditor {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
@@ -943,7 +947,7 @@ impl TerminalTransaction {
|
||||
}
|
||||
|
||||
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
|
||||
// Ensure that the assistant cannot accidently execute commands that are streamed into the terminal
|
||||
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
|
||||
let input = hunk.replace(CARRIAGE_RETURN, " ");
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(input));
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
[package]
|
||||
name = "assistant_tooling"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_tooling.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
repair_json.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sum_tree.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,85 +0,0 @@
|
||||
# Assistant Tooling
|
||||
|
||||
Bringing Language Model tool calling to GPUI.
|
||||
|
||||
This unlocks:
|
||||
|
||||
- **Structured Extraction** of model responses
|
||||
- **Validation** of model inputs
|
||||
- **Execution** of chosen tools
|
||||
|
||||
## Overview
|
||||
|
||||
Language Models can produce structured outputs that are perfect for calling functions. The most famous of these is OpenAI's tool calling. When making a chat completion you can pass a list of tools available to the model. The model will choose `0..n` tools to help them complete a user's task. It's up to _you_ to create the tools that the model can call.
|
||||
|
||||
> **User**: "Hey I need help with implementing a collapsible panel in GPUI"
|
||||
>
|
||||
> **Assistant**: "Sure, I can help with that. Let me see what I can find."
|
||||
>
|
||||
> `tool_calls: ["name": "query_codebase", arguments: "{ 'query': 'GPUI collapsible panel' }"]`
|
||||
>
|
||||
> `result: "['crates/gpui/src/panel.rs:12: impl Panel { ... }', 'crates/gpui/src/panel.rs:20: impl Panel { ... }']"`
|
||||
>
|
||||
> **Assistant**: "Here are some excerpts from the GPUI codebase that might help you."
|
||||
|
||||
This library is designed to facilitate this interaction mode by allowing you to go from `struct` to `tool` with two simple traits, `LanguageModelTool` and `ToolView`.
|
||||
|
||||
## Using the Tool Registry
|
||||
|
||||
```rust
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(WeatherTool { api_client },
|
||||
})
|
||||
.unwrap(); // You can only register one tool per name
|
||||
|
||||
let completion = cx.update(|cx| {
|
||||
CompletionProvider::get(cx).complete(
|
||||
model_name,
|
||||
messages,
|
||||
Vec::new(),
|
||||
1.0,
|
||||
// The definitions get passed directly to OpenAI when you want
|
||||
// the model to be able to call your tool
|
||||
tool_registry.definitions(),
|
||||
)
|
||||
});
|
||||
|
||||
let mut stream = completion?.await?;
|
||||
|
||||
let mut message = AssistantMessage::new();
|
||||
|
||||
while let Some(delta) = stream.next().await {
|
||||
// As messages stream in, you'll get both assistant content
|
||||
if let Some(content) = &delta.content {
|
||||
message
|
||||
.body
|
||||
.update(cx, |message, cx| message.append(&content, cx));
|
||||
}
|
||||
|
||||
// And tool calls!
|
||||
for tool_call_delta in delta.tool_calls {
|
||||
let index = tool_call_delta.index as usize;
|
||||
if index >= message.tool_calls.len() {
|
||||
message.tool_calls.resize_with(index + 1, Default::default);
|
||||
}
|
||||
let tool_call = &mut message.tool_calls[index];
|
||||
|
||||
// Build up an ID
|
||||
if let Some(id) = &tool_call_delta.id {
|
||||
tool_call.id.push_str(id);
|
||||
}
|
||||
|
||||
tool_registry.update_tool_call(
|
||||
tool_call,
|
||||
tool_call_delta.name.as_deref(),
|
||||
tool_call_delta.arguments.as_deref(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once the stream of tokens is complete, you can exexute the tool call by calling `tool_registry.execute_tool_call(tool_call, cx)`, which returns a `Task<Result<()>>`.
|
||||
|
||||
As the tokens stream in and tool calls are executed, your `ToolView` will get updates. Render each tool call by passing that `tool_call` in to `tool_registry.render_tool_call(tool_call, cx)`. The final message for the model can be pulled by calling `self.tool_registry.content_for_tool_call( tool_call, &mut project_context, cx, )`.
|
||||
@@ -1,13 +0,0 @@
|
||||
mod attachment_registry;
|
||||
mod project_context;
|
||||
mod tool_registry;
|
||||
|
||||
pub use attachment_registry::{
|
||||
AttachmentOutput, AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment,
|
||||
UserAttachment,
|
||||
};
|
||||
pub use project_context::ProjectContext;
|
||||
pub use tool_registry::{
|
||||
LanguageModelTool, SavedToolFunctionCall, ToolFunctionCall, ToolFunctionDefinition,
|
||||
ToolRegistry, ToolView,
|
||||
};
|
||||
@@ -1,234 +0,0 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use futures::future::join_all;
|
||||
use gpui::{AnyView, Render, Task, View, WindowContext};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct AttachmentRegistry {
|
||||
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
|
||||
}
|
||||
|
||||
pub trait AttachmentOutput {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
}
|
||||
|
||||
pub trait LanguageModelAttachment {
|
||||
type Output: DeserializeOwned + Serialize + 'static;
|
||||
type View: Render + AttachmentOutput;
|
||||
|
||||
fn name(&self) -> Arc<str>;
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
/// A collected attachment from running an attachment tool
|
||||
pub struct UserAttachment {
|
||||
pub view: AnyView,
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedUserAttachment {
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
}
|
||||
|
||||
/// Internal representation of an attachment tool to allow us to treat them dynamically
|
||||
struct RegisteredAttachment {
|
||||
name: Arc<str>,
|
||||
enabled: AtomicBool,
|
||||
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
|
||||
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
|
||||
}
|
||||
|
||||
impl AttachmentRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_attachments: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
|
||||
let attachment = Arc::new(attachment);
|
||||
|
||||
let call = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
let result = attachment.run(cx);
|
||||
let attachment = attachment.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<A::Output> = result.await;
|
||||
let serialized_output =
|
||||
result
|
||||
.as_ref()
|
||||
.map_err(ToString::to_string)
|
||||
.and_then(|output| {
|
||||
Ok(RawValue::from_string(
|
||||
serde_json::to_string(output).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.unwrap())
|
||||
});
|
||||
|
||||
let view = cx.update(|cx| attachment.view(result, cx))?;
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: attachment.name(),
|
||||
view: view.into(),
|
||||
generate_fn: generate::<A>,
|
||||
serialized_output,
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let deserialize = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
|
||||
let serialized_output = saved_attachment.serialized_output.clone();
|
||||
let output = match &serialized_output {
|
||||
Ok(serialized_output) => {
|
||||
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
|
||||
}
|
||||
Err(error) => Err(anyhow!("{error}")),
|
||||
};
|
||||
let view = attachment.view(output, cx).into();
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: saved_attachment.name.clone(),
|
||||
view,
|
||||
serialized_output,
|
||||
generate_fn: generate::<A>,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
self.registered_attachments.insert(
|
||||
TypeId::of::<A>(),
|
||||
RegisteredAttachment {
|
||||
name: attachment.name(),
|
||||
call,
|
||||
deserialize,
|
||||
enabled: AtomicBool::new(true),
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
||||
fn generate<T: LanguageModelAttachment>(
|
||||
view: AnyView,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
view.downcast::<T::View>()
|
||||
.unwrap()
|
||||
.update(cx, |view, cx| T::View::generate(view, project, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
is_enabled: bool,
|
||||
) {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.store(is_enabled, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(&self) -> bool {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.load(SeqCst)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<UserAttachment>> {
|
||||
let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) else {
|
||||
return Task::ready(Err(anyhow!("no attachment tool")));
|
||||
};
|
||||
|
||||
(attachment.call)(cx)
|
||||
}
|
||||
|
||||
pub fn call_all_attachment_tools(
|
||||
self: Arc<Self>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Task<Result<Vec<UserAttachment>>> {
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let attachment_tasks = cx.update(|cx| {
|
||||
let mut tasks = Vec::new();
|
||||
for attachment in this
|
||||
.registered_attachments
|
||||
.values()
|
||||
.filter(|attachment| attachment.enabled.load(SeqCst))
|
||||
{
|
||||
tasks.push((attachment.call)(cx))
|
||||
}
|
||||
|
||||
tasks
|
||||
})?;
|
||||
|
||||
let attachments = join_all(attachment_tasks.into_iter()).await;
|
||||
|
||||
Ok(attachments
|
||||
.into_iter()
|
||||
.filter_map(|attachment| attachment.log_err())
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_user_attachment(
|
||||
&self,
|
||||
user_attachment: &UserAttachment,
|
||||
) -> SavedUserAttachment {
|
||||
SavedUserAttachment {
|
||||
name: user_attachment.name.clone(),
|
||||
serialized_output: user_attachment.serialized_output.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_user_attachment(
|
||||
&self,
|
||||
saved_user_attachment: SavedUserAttachment,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<UserAttachment> {
|
||||
if let Some(registered_attachment) = self
|
||||
.registered_attachments
|
||||
.values()
|
||||
.find(|attachment| attachment.name == saved_user_attachment.name)
|
||||
{
|
||||
(registered_attachment.deserialize)(&saved_user_attachment, cx)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"no attachment tool for name {}",
|
||||
saved_user_attachment.name
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAttachment {
|
||||
pub fn generate(&self, output: &mut ProjectContext, cx: &mut WindowContext) -> Option<String> {
|
||||
let result = (self.generate_fn)(self.view.clone(), output, cx);
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use project::{Fs, Project, ProjectPath, Worktree};
|
||||
use std::{cmp::Ordering, fmt::Write as _, ops::Range, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
|
||||
pub struct ProjectContext {
|
||||
files: TreeMap<ProjectPath, PathState>,
|
||||
project: WeakModel<Project>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum PathState {
|
||||
PathOnly,
|
||||
EntireFile,
|
||||
Excerpts { ranges: Vec<Range<usize>> },
|
||||
}
|
||||
|
||||
impl ProjectContext {
|
||||
pub fn new(project: WeakModel<Project>, fs: Arc<dyn Fs>) -> Self {
|
||||
Self {
|
||||
files: TreeMap::default(),
|
||||
fs,
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path(&mut self, project_path: ProjectPath) {
|
||||
if self.files.get(&project_path).is_none() {
|
||||
self.files.insert(project_path, PathState::PathOnly);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_excerpts(&mut self, project_path: ProjectPath, new_ranges: &[Range<usize>]) {
|
||||
let previous_state = self
|
||||
.files
|
||||
.get(&project_path)
|
||||
.unwrap_or(&PathState::PathOnly);
|
||||
|
||||
let mut ranges = match previous_state {
|
||||
PathState::EntireFile => return,
|
||||
PathState::PathOnly => Vec::new(),
|
||||
PathState::Excerpts { ranges } => ranges.to_vec(),
|
||||
};
|
||||
|
||||
for new_range in new_ranges {
|
||||
let ix = ranges.binary_search_by(|probe| {
|
||||
if probe.end < new_range.start {
|
||||
Ordering::Less
|
||||
} else if probe.start > new_range.end {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
|
||||
match ix {
|
||||
Ok(mut ix) => {
|
||||
let existing = &mut ranges[ix];
|
||||
existing.start = existing.start.min(new_range.start);
|
||||
existing.end = existing.end.max(new_range.end);
|
||||
while ix + 1 < ranges.len() && ranges[ix + 1].start <= ranges[ix].end {
|
||||
ranges[ix].end = ranges[ix].end.max(ranges[ix + 1].end);
|
||||
ranges.remove(ix + 1);
|
||||
}
|
||||
while ix > 0 && ranges[ix - 1].end >= ranges[ix].start {
|
||||
ranges[ix].start = ranges[ix].start.min(ranges[ix - 1].start);
|
||||
ranges.remove(ix - 1);
|
||||
ix -= 1;
|
||||
}
|
||||
}
|
||||
Err(ix) => {
|
||||
ranges.insert(ix, new_range.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files
|
||||
.insert(project_path, PathState::Excerpts { ranges });
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, project_path: ProjectPath) {
|
||||
self.files.insert(project_path, PathState::EntireFile);
|
||||
}
|
||||
|
||||
pub fn generate_system_message(&self, cx: &mut AppContext) -> Task<Result<String>> {
|
||||
let project = self
|
||||
.project
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("project dropped"));
|
||||
let files = self.files.clone();
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let project = project?;
|
||||
let mut result = "project structure:\n".to_string();
|
||||
|
||||
let mut last_worktree: Option<Model<Worktree>> = None;
|
||||
for (project_path, path_state) in files.iter() {
|
||||
if let Some(worktree) = &last_worktree {
|
||||
if worktree.read_with(&cx, |tree, _| tree.id())? != project_path.worktree_id {
|
||||
last_worktree = None;
|
||||
}
|
||||
}
|
||||
|
||||
let worktree;
|
||||
if let Some(last_worktree) = &last_worktree {
|
||||
worktree = last_worktree.clone();
|
||||
} else if let Some(tree) = project.read_with(&cx, |project, cx| {
|
||||
project.worktree_for_id(project_path.worktree_id, cx)
|
||||
})? {
|
||||
worktree = tree;
|
||||
last_worktree = Some(worktree.clone());
|
||||
let worktree_name =
|
||||
worktree.read_with(&cx, |tree, _cx| tree.root_name().to_string())?;
|
||||
writeln!(&mut result, "# {}", worktree_name).unwrap();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let worktree_abs_path = worktree.read_with(&cx, |tree, _cx| tree.abs_path())?;
|
||||
let path = &project_path.path;
|
||||
writeln!(&mut result, "## {}", path.display()).unwrap();
|
||||
|
||||
match path_state {
|
||||
PathState::PathOnly => {}
|
||||
PathState::EntireFile => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
writeln!(&mut result, "~~~\n{text}\n~~~").unwrap();
|
||||
}
|
||||
PathState::Excerpts { ranges } => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
|
||||
// Assumption: ranges are in order, not overlapping
|
||||
let mut prev_range_end = 0;
|
||||
for range in ranges {
|
||||
if range.start > prev_range_end {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
prev_range_end = range.end;
|
||||
}
|
||||
|
||||
let mut start = range.start;
|
||||
let mut end = range.end.min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
result.push_str(&text[start..end]);
|
||||
if !result.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if prev_range_end < text.len() {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
}
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_system_message_generation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let file_3_contents = r#"
|
||||
fn test1() {}
|
||||
fn test2() {}
|
||||
fn test3() {}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
json!({
|
||||
"root1": {
|
||||
"lib": {
|
||||
"file1.rs": "mod example;",
|
||||
"file2.rs": "",
|
||||
},
|
||||
"test": {
|
||||
"file3.rs": file_3_contents,
|
||||
}
|
||||
},
|
||||
"root2": {
|
||||
"src": {
|
||||
"main.rs": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs.clone(),
|
||||
["/code/root1".as_ref(), "/code/root2".as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let worktree_ids = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let mut ax = ProjectContext::new(project.downgrade(), fs);
|
||||
|
||||
ax.add_file(ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("lib/file1.rs").into(),
|
||||
});
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
|
||||
ax.add_excerpts(
|
||||
ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("test/file3.rs").into(),
|
||||
},
|
||||
&[
|
||||
file_3_contents.find("fn test2").unwrap()
|
||||
..file_3_contents.find("fn test3").unwrap(),
|
||||
],
|
||||
);
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
## test/file3.rs
|
||||
~~~
|
||||
...
|
||||
fn test2() {}
|
||||
...
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,526 +0,0 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AnyElement, AnyView, IntoElement, Render, Task, View, WindowContext};
|
||||
use repair_json::repair;
|
||||
use schemars::{schema::RootSchema, schema_for, JsonSchema};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
mem,
|
||||
sync::atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
use ui::ViewContext;
|
||||
|
||||
pub struct ToolRegistry {
|
||||
registered_tools: HashMap<String, RegisteredTool>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ToolFunctionCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
state: ToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum ToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool(Box<dyn InternalToolView>),
|
||||
ExecutedTool(Box<dyn InternalToolView>),
|
||||
}
|
||||
|
||||
trait InternalToolView {
|
||||
fn view(&self) -> AnyView;
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext);
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>>;
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>>;
|
||||
fn deserialize_output(&self, raw_value: &RawValue, cx: &mut WindowContext) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct SavedToolFunctionCall {
|
||||
id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
state: SavedToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
enum SavedToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool,
|
||||
ExecutedTool(Box<RawValue>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ToolFunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: RootSchema,
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool {
|
||||
type View: ToolView;
|
||||
|
||||
/// Returns the name of the tool.
|
||||
///
|
||||
/// This name is exposed to the language model to allow the model to pick
|
||||
/// which tools to use. As this name is used to identify the tool within a
|
||||
/// tool registry, it should be unique.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the description of the tool.
|
||||
///
|
||||
/// This can be used to _prompt_ the model as to what the tool does.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
|
||||
fn definition(&self) -> ToolFunctionDefinition {
|
||||
let root_schema = schema_for!(<Self::View as ToolView>::Input);
|
||||
|
||||
ToolFunctionDefinition {
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
parameters: root_schema,
|
||||
}
|
||||
}
|
||||
|
||||
/// A view of the output of running the tool, for displaying to the user.
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
pub trait ToolView: Render {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: DeserializeOwned + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type SerializedState: DeserializeOwned + Serialize;
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut ViewContext<Self>) -> String;
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>);
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
|
||||
|
||||
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState;
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
struct RegisteredTool {
|
||||
enabled: AtomicBool,
|
||||
type_id: TypeId,
|
||||
build_view: Box<dyn Fn(&mut WindowContext) -> Box<dyn InternalToolView>>,
|
||||
definition: ToolFunctionDefinition,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_enabled<T: 'static + LanguageModelTool>(&self, is_enabled: bool) {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
tool.enabled.store(is_enabled, SeqCst);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tool_enabled<T: 'static + LanguageModelTool>(&self) -> bool {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
return tool.enabled.load(SeqCst);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> Vec<ToolFunctionDefinition> {
|
||||
self.registered_tools
|
||||
.values()
|
||||
.filter(|tool| tool.enabled.load(SeqCst))
|
||||
.map(|tool| tool.definition.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update_tool_call(
|
||||
&self,
|
||||
call: &mut ToolFunctionCall,
|
||||
name: Option<&str>,
|
||||
arguments: Option<&str>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(name) = name {
|
||||
call.name.push_str(name);
|
||||
}
|
||||
if let Some(arguments) = arguments {
|
||||
if call.arguments.is_empty() {
|
||||
if let Some(tool) = self.registered_tools.get(&call.name) {
|
||||
let view = (tool.build_view)(cx);
|
||||
call.state = ToolFunctionCallState::KnownTool(view);
|
||||
} else {
|
||||
call.state = ToolFunctionCallState::NoSuchTool;
|
||||
}
|
||||
}
|
||||
call.arguments.push_str(arguments);
|
||||
|
||||
if let ToolFunctionCallState::KnownTool(view) = &call.state {
|
||||
if let Ok(repaired_arguments) = repair(call.arguments.clone()) {
|
||||
view.try_set_input(&repaired_arguments, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_tool_call(
|
||||
&self,
|
||||
tool_call: &mut ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if let ToolFunctionCallState::KnownTool(view) = mem::take(&mut tool_call.state) {
|
||||
let task = view.execute(cx);
|
||||
tool_call.state = ToolFunctionCallState::ExecutedTool(view);
|
||||
Some(task)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
Some(ui::Label::new("No such tool").into_any_element())
|
||||
}
|
||||
ToolFunctionCallState::Initializing => None,
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
Some(view.view().into_any_element())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_for_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
project_context: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::Initializing => String::new(),
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
format!("No such tool: {}", tool_call.name)
|
||||
}
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
view.generate(project_context, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_tool_call(
|
||||
&self,
|
||||
call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<SavedToolFunctionCall> {
|
||||
Ok(SavedToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
ToolFunctionCallState::Initializing => SavedToolFunctionCallState::Initializing,
|
||||
ToolFunctionCallState::NoSuchTool => SavedToolFunctionCallState::NoSuchTool,
|
||||
ToolFunctionCallState::KnownTool(_) => SavedToolFunctionCallState::KnownTool,
|
||||
ToolFunctionCallState::ExecutedTool(view) => {
|
||||
SavedToolFunctionCallState::ExecutedTool(view.serialize_output(cx)?)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize_tool_call(
|
||||
&self,
|
||||
call: &SavedToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<ToolFunctionCall> {
|
||||
let Some(tool) = self.registered_tools.get(&call.name) else {
|
||||
return Err(anyhow!("no such tool {}", call.name));
|
||||
};
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
SavedToolFunctionCallState::Initializing => ToolFunctionCallState::Initializing,
|
||||
SavedToolFunctionCallState::NoSuchTool => ToolFunctionCallState::NoSuchTool,
|
||||
SavedToolFunctionCallState::KnownTool => {
|
||||
log::error!("Deserialized tool that had not executed");
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
ToolFunctionCallState::KnownTool(view)
|
||||
}
|
||||
SavedToolFunctionCallState::ExecutedTool(output) => {
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
view.deserialize_output(output, cx)?;
|
||||
ToolFunctionCallState::ExecutedTool(view)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
|
||||
let name = tool.name();
|
||||
let registered_tool = RegisteredTool {
|
||||
type_id: TypeId::of::<T>(),
|
||||
definition: tool.definition(),
|
||||
enabled: AtomicBool::new(true),
|
||||
build_view: Box::new(move |cx: &mut WindowContext| Box::new(tool.view(cx))),
|
||||
};
|
||||
|
||||
let previous = self.registered_tools.insert(name.clone(), registered_tool);
|
||||
if previous.is_some() {
|
||||
return Err(anyhow!("already registered a tool with name {}", name));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToolView> InternalToolView for View<T> {
|
||||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
|
||||
self.update(cx, |view, cx| view.generate(project, cx))
|
||||
}
|
||||
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext) {
|
||||
if let Ok(input) = serde_json::from_str::<T::Input>(input) {
|
||||
self.update(cx, |view, cx| {
|
||||
view.set_input(input, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>> {
|
||||
self.update(cx, |view, cx| view.execute(cx))
|
||||
}
|
||||
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>> {
|
||||
let output = self.update(cx, |view, cx| view.serialize(cx));
|
||||
Ok(RawValue::from_string(serde_json::to_string(&output)?)?)
|
||||
}
|
||||
|
||||
fn deserialize_output(&self, output: &RawValue, cx: &mut WindowContext) -> Result<()> {
|
||||
let state = serde_json::from_str::<T::SerializedState>(output.get())?;
|
||||
self.update(cx, |view, cx| view.deserialize(state, cx))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ToolFunctionDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let schema = serde_json::to_string(&self.parameters).ok();
|
||||
let schema = schema.unwrap_or("None".to_string());
|
||||
write!(f, "Name: {}:\n", self.name)?;
|
||||
write!(f, "Description: {}\n", self.description)?;
|
||||
write!(f, "Parameters: {}", schema)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{div, prelude::*, Render, TestAppContext};
|
||||
use gpui::{EmptyView, View};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
struct WeatherQuery {
|
||||
location: String,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct WeatherResult {
|
||||
location: String,
|
||||
temperature: f64,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherView {
|
||||
input: Option<WeatherQuery>,
|
||||
result: Option<WeatherResult>,
|
||||
|
||||
// Fake API call
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
impl WeatherView {
|
||||
fn new(current_weather: WeatherResult) -> Self {
|
||||
Self {
|
||||
input: None,
|
||||
result: None,
|
||||
current_weather,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for WeatherView {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
match self.result {
|
||||
Some(ref result) => div()
|
||||
.child(format!("temperature: {}", result.temperature))
|
||||
.into_any_element(),
|
||||
None => div().child("Calculating weather...").into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolView for WeatherView {
|
||||
type Input = WeatherQuery;
|
||||
|
||||
type SerializedState = WeatherResult;
|
||||
|
||||
fn generate(&self, _output: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
|
||||
serde_json::to_string(&self.result).unwrap()
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
self.input = Some(input);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
let input = self.input.as_ref().unwrap();
|
||||
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
self.result = Some(weather);
|
||||
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
self.current_weather.clone()
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
self.current_weather = output;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WeatherTool {
|
||||
type View = WeatherView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"get_current_weather".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Fetches the current weather for a given location.".to_string()
|
||||
}
|
||||
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| WeatherView::new(self.current_weather.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
|
||||
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry
|
||||
.register(WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let definitions = registry.definitions();
|
||||
assert_eq!(
|
||||
definitions,
|
||||
[ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: serde_json::from_value(json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}))
|
||||
.unwrap(),
|
||||
}]
|
||||
);
|
||||
|
||||
let mut call = ToolFunctionCall {
|
||||
id: "the-id".to_string(),
|
||||
name: "get_cur".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let task = cx.update(|cx| {
|
||||
registry.update_tool_call(
|
||||
&mut call,
|
||||
Some("rent_weather"),
|
||||
Some(r#"{"location": "San Francisco","#),
|
||||
cx,
|
||||
);
|
||||
registry.update_tool_call(&mut call, None, Some(r#" "unit": "Celsius"}"#), cx);
|
||||
registry.execute_tool_call(&mut call, cx).unwrap()
|
||||
});
|
||||
task.await.unwrap();
|
||||
|
||||
match &call.state {
|
||||
ToolFunctionCallState::ExecutedTool(_view) => {}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,6 @@ struct UpdateRequestBody {
|
||||
installation_id: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
telemetry: bool,
|
||||
is_staff: Option<bool>,
|
||||
destination: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
@@ -577,27 +575,18 @@ async fn download_remote_server_binary(
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let mut target_file = File::create(&target_path).await?;
|
||||
let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
|
||||
let telemetry = Client::global(cx).telemetry().clone();
|
||||
let is_staff = telemetry.is_staff();
|
||||
let installation_id = telemetry.installation_id();
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry_enabled,
|
||||
is_staff,
|
||||
)
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry: telemetry_enabled,
|
||||
is_staff,
|
||||
destination: "remote",
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
@@ -613,28 +602,19 @@ async fn download_release(
|
||||
) -> Result<()> {
|
||||
let mut target_file = File::create(&target_path).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
|
||||
let telemetry = Client::global(cx).telemetry().clone();
|
||||
let is_staff = telemetry.is_staff();
|
||||
let installation_id = telemetry.installation_id();
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry_enabled,
|
||||
is_staff,
|
||||
)
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry: telemetry_enabled,
|
||||
is_staff,
|
||||
destination: "local",
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
|
||||
@@ -493,7 +493,7 @@ impl Room {
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade() {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?;
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
@@ -942,7 +942,7 @@ impl Room {
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
let _ = this.leave(cx);
|
||||
let _ = this.leave(cx).detach();
|
||||
}
|
||||
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
|
||||
@@ -7,8 +7,9 @@ pub mod user;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use async_tungstenite::tungstenite::{
|
||||
client::IntoClientRequest,
|
||||
error::Error as WebsocketError,
|
||||
http::{Request, StatusCode},
|
||||
http::{HeaderValue, Request, StatusCode},
|
||||
};
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
@@ -235,6 +236,8 @@ pub enum EstablishConnectionError {
|
||||
#[error("{0}")]
|
||||
Http(#[from] http_client::Error),
|
||||
#[error("{0}")]
|
||||
InvalidHeaderValue(#[from] async_tungstenite::tungstenite::http::header::InvalidHeaderValue),
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
Websocket(#[from] async_tungstenite::tungstenite::http::Error),
|
||||
@@ -1159,19 +1162,24 @@ impl Client {
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let request = Request::builder()
|
||||
.header("Authorization", credentials.authorization_header())
|
||||
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION)
|
||||
.header("x-zed-app-version", app_version)
|
||||
.header(
|
||||
"x-zed-release-channel",
|
||||
release_channel.map(|r| r.dev_name()).unwrap_or("unknown"),
|
||||
);
|
||||
|
||||
let http = self.http.clone();
|
||||
let credentials = credentials.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
cx.background_executor().spawn(async move {
|
||||
use HttpOrHttps::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HttpOrHttps {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
let mut rpc_url = rpc_url.await?;
|
||||
let url_scheme = match rpc_url.scheme() {
|
||||
"https" => Https,
|
||||
"http" => Http,
|
||||
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
|
||||
};
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1180,10 +1188,37 @@ impl Client {
|
||||
|
||||
log::info!("connected to rpc endpoint {}", rpc_url);
|
||||
|
||||
match rpc_url.scheme() {
|
||||
"https" => {
|
||||
rpc_url.set_scheme("wss").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
rpc_url
|
||||
.set_scheme(match url_scheme {
|
||||
Https => "wss",
|
||||
Http => "ws",
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// We call `into_client_request` to let `tungstenite` construct the WebSocket request
|
||||
// for us from the RPC URL.
|
||||
//
|
||||
// Among other things, it will generate and set a `Sec-WebSocket-Key` header for us.
|
||||
let mut request = rpc_url.into_client_request()?;
|
||||
|
||||
// We then modify the request to add our desired headers.
|
||||
let request_headers = request.headers_mut();
|
||||
request_headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&credentials.authorization_header())?,
|
||||
);
|
||||
request_headers.insert(
|
||||
"x-zed-protocol-version",
|
||||
HeaderValue::from_str(&rpc::PROTOCOL_VERSION.to_string())?,
|
||||
);
|
||||
request_headers.insert("x-zed-app-version", HeaderValue::from_str(&app_version)?);
|
||||
request_headers.insert(
|
||||
"x-zed-release-channel",
|
||||
HeaderValue::from_str(&release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||
);
|
||||
|
||||
match url_scheme {
|
||||
Https => {
|
||||
let (stream, _) =
|
||||
async_tungstenite::async_std::client_async_tls(request, stream).await?;
|
||||
Ok(Connection::new(
|
||||
@@ -1192,9 +1227,7 @@ impl Client {
|
||||
.sink_map_err(|error| anyhow!(error)),
|
||||
))
|
||||
}
|
||||
"http" => {
|
||||
rpc_url.set_scheme("ws").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
Http => {
|
||||
let (stream, _) = async_tungstenite::client_async(request, stream).await?;
|
||||
Ok(Connection::new(
|
||||
stream
|
||||
@@ -1202,7 +1235,6 @@ impl Client {
|
||||
.sink_map_err(|error| anyhow!(error)),
|
||||
))
|
||||
}
|
||||
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1410,7 +1442,7 @@ impl Client {
|
||||
self.peer.send(self.connection_id()?, message)
|
||||
}
|
||||
|
||||
fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
pub fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
let connection_id = self.connection_id()?;
|
||||
self.peer.send_dynamic(connection_id, envelope)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ chrono.workspace = true
|
||||
clock.workspace = true
|
||||
clickhouse.workspace = true
|
||||
collections.workspace = true
|
||||
dashmap = "5.4"
|
||||
dashmap.workspace = true
|
||||
envy = "0.4.2"
|
||||
futures.workspace = true
|
||||
google_ai.workspace = true
|
||||
@@ -47,7 +47,7 @@ prost.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
rpc.workspace = true
|
||||
scrypt = "0.7"
|
||||
scrypt = "0.11"
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
semantic_version.workspace = true
|
||||
semver.workspace = true
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{anyhow, Result};
|
||||
use rpc::proto;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub fn language_model_request_to_open_ai(
|
||||
request: proto::CompleteWithLanguageModel,
|
||||
@@ -21,25 +20,7 @@ pub fn language_model_request_to_open_ai(
|
||||
proto::LanguageModelRole::LanguageModelAssistant => {
|
||||
open_ai::RequestMessage::Assistant {
|
||||
content: Some(message.content),
|
||||
tool_calls: message
|
||||
.tool_calls
|
||||
.into_iter()
|
||||
.filter_map(|call| {
|
||||
Some(open_ai::ToolCall {
|
||||
id: call.id,
|
||||
content: match call.variant? {
|
||||
proto::tool_call::Variant::Function(f) => {
|
||||
open_ai::ToolCallContent::Function {
|
||||
function: open_ai::FunctionContent {
|
||||
name: f.name,
|
||||
arguments: f.arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
tool_calls: Vec::new(),
|
||||
}
|
||||
}
|
||||
proto::LanguageModelRole::LanguageModelSystem => {
|
||||
@@ -47,12 +28,6 @@ pub fn language_model_request_to_open_ai(
|
||||
content: message.content,
|
||||
}
|
||||
}
|
||||
proto::LanguageModelRole::LanguageModelTool => open_ai::RequestMessage::Tool {
|
||||
tool_call_id: message
|
||||
.tool_call_id
|
||||
.ok_or_else(|| anyhow!("tool message is missing tool call id"))?,
|
||||
content: message.content,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(openai_message)
|
||||
@@ -61,32 +36,8 @@ pub fn language_model_request_to_open_ai(
|
||||
stream: true,
|
||||
stop: request.stop,
|
||||
temperature: request.temperature,
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
Some(match tool.variant? {
|
||||
proto::chat_completion_tool::Variant::Function(f) => {
|
||||
open_ai::ToolDefinition::Function {
|
||||
function: open_ai::FunctionDefinition {
|
||||
name: f.name,
|
||||
description: f.description,
|
||||
parameters: if let Some(params) = &f.parameters {
|
||||
Some(
|
||||
serde_json::from_str(params)
|
||||
.context("failed to deserialize tool parameters")
|
||||
.log_err()?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
tool_choice: request.tool_choice,
|
||||
tool_choice: None,
|
||||
tools: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,9 +69,6 @@ pub fn language_model_request_message_to_google_ai(
|
||||
proto::LanguageModelRole::LanguageModelUser => google_ai::Role::User,
|
||||
proto::LanguageModelRole::LanguageModelAssistant => google_ai::Role::Model,
|
||||
proto::LanguageModelRole::LanguageModelSystem => google_ai::Role::User,
|
||||
proto::LanguageModelRole::LanguageModelTool => {
|
||||
Err(anyhow!("we don't handle tool calls with google ai yet"))?
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod contributors;
|
||||
pub mod events;
|
||||
pub mod extensions;
|
||||
pub mod ips_file;
|
||||
@@ -5,13 +6,13 @@ pub mod slack;
|
||||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{ContributorSelector, User, UserId},
|
||||
db::{User, UserId},
|
||||
rpc, AppState, Error, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{self, Path, Query},
|
||||
extract::{Path, Query},
|
||||
http::{self, Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::IntoResponse,
|
||||
@@ -19,7 +20,6 @@ use axum::{
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use axum_extra::response::ErasedJson;
|
||||
use chrono::SecondsFormat;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tower::ServiceBuilder;
|
||||
@@ -31,8 +31,7 @@ pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Rou
|
||||
.route("/user", get(get_authenticated_user))
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route("/contributors", get(get_contributors).post(add_contributor))
|
||||
.route("/contributor", get(check_is_contributor))
|
||||
.merge(contributors::router())
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(Extension(state))
|
||||
@@ -126,66 +125,6 @@ async fn get_rpc_server_snapshot(
|
||||
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
|
||||
}
|
||||
|
||||
async fn get_contributors(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<String>>> {
|
||||
Ok(Json(app.db.get_contributors().await?))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CheckIsContributorParams {
|
||||
github_user_id: Option<i32>,
|
||||
github_login: Option<String>,
|
||||
}
|
||||
|
||||
impl CheckIsContributorParams {
|
||||
fn as_contributor_selector(self) -> Result<ContributorSelector> {
|
||||
if let Some(github_user_id) = self.github_user_id {
|
||||
return Ok(ContributorSelector::GitHubUserId { github_user_id });
|
||||
}
|
||||
|
||||
if let Some(github_login) = self.github_login {
|
||||
return Ok(ContributorSelector::GitHubLogin { github_login });
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"must be one of `github_user_id` or `github_login`."
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CheckIsContributorResponse {
|
||||
signed_at: Option<String>,
|
||||
}
|
||||
|
||||
async fn check_is_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<CheckIsContributorParams>,
|
||||
) -> Result<Json<CheckIsContributorResponse>> {
|
||||
let params = params.as_contributor_selector()?;
|
||||
Ok(Json(CheckIsContributorResponse {
|
||||
signed_at: app
|
||||
.db
|
||||
.get_contributor_sign_timestamp(¶ms)
|
||||
.await?
|
||||
.map(|ts| ts.and_utc().to_rfc3339_opts(SecondsFormat::Millis, true)),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn add_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(params): extract::Json<AuthenticatedUserParams>,
|
||||
) -> Result<()> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
app.db
|
||||
.add_contributor(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
initial_channel_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateAccessTokenQueryParams {
|
||||
public_key: String,
|
||||
|
||||
121
crates/collab/src/api/contributors.rs
Normal file
121
crates/collab/src/api/contributors.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{self, Query},
|
||||
routing::get,
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use chrono::{NaiveDateTime, SecondsFormat};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::AuthenticatedUserParams;
|
||||
use crate::db::ContributorSelector;
|
||||
use crate::{AppState, Result};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/contributors", get(get_contributors).post(add_contributor))
|
||||
.route("/contributor", get(check_is_contributor))
|
||||
}
|
||||
|
||||
async fn get_contributors(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<String>>> {
|
||||
Ok(Json(app.db.get_contributors().await?))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CheckIsContributorParams {
|
||||
github_user_id: Option<i32>,
|
||||
github_login: Option<String>,
|
||||
}
|
||||
|
||||
impl CheckIsContributorParams {
|
||||
fn as_contributor_selector(self) -> Result<ContributorSelector> {
|
||||
if let Some(github_user_id) = self.github_user_id {
|
||||
return Ok(ContributorSelector::GitHubUserId { github_user_id });
|
||||
}
|
||||
|
||||
if let Some(github_login) = self.github_login {
|
||||
return Ok(ContributorSelector::GitHubLogin { github_login });
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"must be one of `github_user_id` or `github_login`."
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CheckIsContributorResponse {
|
||||
signed_at: Option<String>,
|
||||
}
|
||||
|
||||
async fn check_is_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<CheckIsContributorParams>,
|
||||
) -> Result<Json<CheckIsContributorResponse>> {
|
||||
let params = params.as_contributor_selector()?;
|
||||
|
||||
if RenovateBot::is_renovate_bot(¶ms) {
|
||||
return Ok(Json(CheckIsContributorResponse {
|
||||
signed_at: Some(
|
||||
RenovateBot::created_at()
|
||||
.and_utc()
|
||||
.to_rfc3339_opts(SecondsFormat::Millis, true),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Json(CheckIsContributorResponse {
|
||||
signed_at: app
|
||||
.db
|
||||
.get_contributor_sign_timestamp(¶ms)
|
||||
.await?
|
||||
.map(|ts| ts.and_utc().to_rfc3339_opts(SecondsFormat::Millis, true)),
|
||||
}))
|
||||
}
|
||||
|
||||
/// The Renovate bot GitHub user (`renovate[bot]`).
|
||||
///
|
||||
/// https://api.github.com/users/renovate[bot]
|
||||
struct RenovateBot;
|
||||
|
||||
impl RenovateBot {
|
||||
const LOGIN: &'static str = "renovate[bot]";
|
||||
const USER_ID: i32 = 29139614;
|
||||
|
||||
/// Returns the `created_at` timestamp for the Renovate bot user.
|
||||
fn created_at() -> &'static NaiveDateTime {
|
||||
static CREATED_AT: OnceLock<NaiveDateTime> = OnceLock::new();
|
||||
CREATED_AT.get_or_init(|| {
|
||||
chrono::DateTime::parse_from_rfc3339("2017-06-02T07:04:12Z")
|
||||
.expect("failed to parse 'created_at' for 'renovate[bot]'")
|
||||
.naive_utc()
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether the given contributor selector corresponds to the Renovate bot user.
|
||||
fn is_renovate_bot(contributor: &ContributorSelector) -> bool {
|
||||
match contributor {
|
||||
ContributorSelector::GitHubLogin { github_login } => github_login == Self::LOGIN,
|
||||
ContributorSelector::GitHubUserId { github_user_id } => {
|
||||
github_user_id == &Self::USER_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(params): extract::Json<AuthenticatedUserParams>,
|
||||
) -> Result<()> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
app.db
|
||||
.add_contributor(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
initial_channel_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use axum::{
|
||||
middleware::Next,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use base64::prelude::*;
|
||||
use prometheus::{exponential_buckets, register_histogram, Histogram};
|
||||
pub use rpc::auth::random_token;
|
||||
use scrypt::{
|
||||
@@ -155,10 +156,7 @@ pub async fn create_access_token(
|
||||
/// protection.
|
||||
pub fn hash_access_token(token: &str) -> String {
|
||||
let digest = sha2::Sha256::digest(token);
|
||||
format!(
|
||||
"$sha256${}",
|
||||
base64::encode_config(digest, base64::URL_SAFE)
|
||||
)
|
||||
format!("$sha256${}", BASE64_URL_SAFE.encode(digest))
|
||||
}
|
||||
|
||||
/// Encrypts the given access token with the given public key to avoid leaking it on the way
|
||||
@@ -402,15 +400,16 @@ mod test {
|
||||
fn previous_hash_access_token(token: &str) -> Result<String> {
|
||||
// Avoid slow hashing in debug mode.
|
||||
let params = if cfg!(debug_assertions) {
|
||||
scrypt::Params::new(1, 1, 1).unwrap()
|
||||
scrypt::Params::new(1, 1, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
|
||||
} else {
|
||||
scrypt::Params::new(14, 8, 1).unwrap()
|
||||
scrypt::Params::new(14, 8, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
|
||||
};
|
||||
|
||||
Ok(Scrypt
|
||||
.hash_password(
|
||||
.hash_password_customized(
|
||||
token.as_bytes(),
|
||||
None,
|
||||
None,
|
||||
params,
|
||||
&SaltString::generate(thread_rng()),
|
||||
)
|
||||
|
||||
@@ -153,7 +153,7 @@ async fn main() -> Result<()> {
|
||||
let signal = async move {
|
||||
// todo(windows):
|
||||
// `ctrl_close` does not work well, because tokio's signal handler always returns soon,
|
||||
// but system termiates the application soon after returning CTRL+CLOSE handler.
|
||||
// but system terminates the application soon after returning CTRL+CLOSE handler.
|
||||
// So we should implement blocking handler to treat CTRL+CLOSE signal.
|
||||
let mut ctrl_break = tokio::signal::windows::ctrl_break()
|
||||
.expect("failed to listen for interrupt signal");
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
executor::Executor,
|
||||
AppState, Error, RateLimit, RateLimiter, Result,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use anyhow::{anyhow, bail, Context as _};
|
||||
use async_tungstenite::tungstenite::{
|
||||
protocol::CloseFrame as TungsteniteCloseFrame, Message as TungsteniteMessage,
|
||||
};
|
||||
@@ -1392,7 +1392,7 @@ pub async fn handle_websocket_request(
|
||||
let socket = socket
|
||||
.map_ok(to_tungstenite_message)
|
||||
.err_into()
|
||||
.with(|message| async move { Ok(to_axum_message(message)) });
|
||||
.with(|message| async move { to_axum_message(message) });
|
||||
let connection = Connection::new(Box::pin(socket));
|
||||
async move {
|
||||
server
|
||||
@@ -4597,37 +4597,19 @@ async fn complete_with_open_ai(
|
||||
.map(|choice| proto::LanguageModelChoiceDelta {
|
||||
index: choice.index,
|
||||
delta: Some(proto::LanguageModelResponseMessage {
|
||||
role: choice.delta.role.map(|role| match role {
|
||||
open_ai::Role::User => LanguageModelRole::LanguageModelUser,
|
||||
open_ai::Role::Assistant => LanguageModelRole::LanguageModelAssistant,
|
||||
open_ai::Role::System => LanguageModelRole::LanguageModelSystem,
|
||||
open_ai::Role::Tool => LanguageModelRole::LanguageModelTool,
|
||||
} as i32),
|
||||
role: choice.delta.role.and_then(|role| match role {
|
||||
open_ai::Role::User => {
|
||||
Some(LanguageModelRole::LanguageModelUser as i32)
|
||||
}
|
||||
open_ai::Role::Assistant => {
|
||||
Some(LanguageModelRole::LanguageModelAssistant as i32)
|
||||
}
|
||||
open_ai::Role::System => {
|
||||
Some(LanguageModelRole::LanguageModelSystem as i32)
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
content: choice.delta.content,
|
||||
tool_calls: choice
|
||||
.delta
|
||||
.tool_calls
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|delta| proto::ToolCallDelta {
|
||||
index: delta.index as u32,
|
||||
id: delta.id,
|
||||
variant: match delta.function {
|
||||
Some(function) => {
|
||||
let name = function.name;
|
||||
let arguments = function.arguments;
|
||||
|
||||
Some(proto::tool_call_delta::Variant::Function(
|
||||
proto::tool_call_delta::FunctionCallDelta {
|
||||
name,
|
||||
arguments,
|
||||
},
|
||||
))
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
finish_reason: choice.finish_reason,
|
||||
})
|
||||
@@ -4679,8 +4661,6 @@ async fn complete_with_google_ai(
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
// Tool calls are not supported for Google
|
||||
tool_calls: Vec::new(),
|
||||
}),
|
||||
finish_reason: candidate.finish_reason.map(|reason| reason.to_string()),
|
||||
})
|
||||
@@ -4697,8 +4677,6 @@ async fn complete_with_anthropic(
|
||||
session: UserSession,
|
||||
api_key: Arc<str>,
|
||||
) -> Result<()> {
|
||||
let model = anthropic::Model::from_id(&request.model)?;
|
||||
|
||||
let mut system_message = String::new();
|
||||
let messages = request
|
||||
.messages
|
||||
@@ -4723,8 +4701,6 @@ async fn complete_with_anthropic(
|
||||
|
||||
None
|
||||
}
|
||||
// We don't yet support tool calls for Anthropic
|
||||
LanguageModelRole::LanguageModelTool => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -4734,7 +4710,7 @@ async fn complete_with_anthropic(
|
||||
anthropic::ANTHROPIC_API_URL,
|
||||
&api_key,
|
||||
anthropic::Request {
|
||||
model,
|
||||
model: request.model,
|
||||
messages,
|
||||
stream: true,
|
||||
system: system_message,
|
||||
@@ -4769,7 +4745,6 @@ async fn complete_with_anthropic(
|
||||
delta: Some(proto::LanguageModelResponseMessage {
|
||||
role: Some(current_role as i32),
|
||||
content: Some(text),
|
||||
tool_calls: Vec::new(),
|
||||
}),
|
||||
finish_reason: None,
|
||||
}],
|
||||
@@ -4786,7 +4761,6 @@ async fn complete_with_anthropic(
|
||||
delta: Some(proto::LanguageModelResponseMessage {
|
||||
role: Some(current_role as i32),
|
||||
content: Some(text),
|
||||
tool_calls: Vec::new(),
|
||||
}),
|
||||
finish_reason: None,
|
||||
}],
|
||||
@@ -5154,8 +5128,8 @@ async fn get_private_user_info(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_axum_message(message: TungsteniteMessage) -> AxumMessage {
|
||||
match message {
|
||||
fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result<AxumMessage> {
|
||||
let message = match message {
|
||||
TungsteniteMessage::Text(payload) => AxumMessage::Text(payload),
|
||||
TungsteniteMessage::Binary(payload) => AxumMessage::Binary(payload),
|
||||
TungsteniteMessage::Ping(payload) => AxumMessage::Ping(payload),
|
||||
@@ -5164,7 +5138,20 @@ fn to_axum_message(message: TungsteniteMessage) -> AxumMessage {
|
||||
code: frame.code.into(),
|
||||
reason: frame.reason,
|
||||
})),
|
||||
}
|
||||
// We should never receive a frame while reading the message, according
|
||||
// to the `tungstenite` maintainers:
|
||||
//
|
||||
// > It cannot occur when you read messages from the WebSocket, but it
|
||||
// > can be used when you want to send the raw frames (e.g. you want to
|
||||
// > send the frames to the WebSocket without composing the full message first).
|
||||
// >
|
||||
// > — https://github.com/snapview/tungstenite-rs/issues/268
|
||||
TungsteniteMessage::Frame(_) => {
|
||||
bail!("received an unexpected frame while reading the message")
|
||||
}
|
||||
};
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(clippy::reversed_empty_ranges)]
|
||||
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
use client::ChannelId;
|
||||
|
||||
@@ -78,7 +78,7 @@ pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-markdown.workspace = true
|
||||
tree-sitter-md.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -533,6 +533,7 @@ impl Render for MessageEditor {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -2190,6 +2190,7 @@ impl CollabPanel {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, StreamExt};
|
||||
use gpui::{AppContext, Global, Model, ModelContext, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
@@ -27,7 +27,7 @@ pub struct LanguageModelCompletionProvider {
|
||||
const MAX_CONCURRENT_COMPLETION_REQUESTS: usize = 4;
|
||||
|
||||
pub struct LanguageModelCompletionResponse {
|
||||
pub inner: BoxStream<'static, Result<String>>,
|
||||
inner: BoxStream<'static, Result<String>>,
|
||||
_lock: SemaphoreGuardArc,
|
||||
}
|
||||
|
||||
@@ -143,11 +143,11 @@ impl LanguageModelCompletionProvider {
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
) -> Option<BoxFuture<'static, Result<usize>>> {
|
||||
if let Some(model) = self.active_model() {
|
||||
model.count_tokens(request, cx)
|
||||
Some(model.count_tokens(request, cx))
|
||||
} else {
|
||||
std::future::ready(Err(anyhow!("No active model set"))).boxed()
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -691,7 +691,7 @@ impl Copilot {
|
||||
{
|
||||
match event {
|
||||
language::Event::Edited => {
|
||||
let _ = registered_buffer.report_changes(&buffer, cx);
|
||||
drop(registered_buffer.report_changes(&buffer, cx));
|
||||
}
|
||||
language::Event::Saved => {
|
||||
server
|
||||
|
||||
@@ -333,7 +333,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -341,7 +341,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
);
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -383,7 +383,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
);
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -408,7 +408,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -416,7 +416,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
);
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -590,7 +590,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -598,7 +598,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
);
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -632,7 +632,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
@@ -640,7 +640,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
);
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -889,7 +889,7 @@ mod tests {
|
||||
three
|
||||
"});
|
||||
|
||||
let _ = handle_completion_request(
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
@@ -897,7 +897,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
);
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -917,7 +917,7 @@ mod tests {
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("o");
|
||||
let _ = handle_completion_request(
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
@@ -925,7 +925,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["completion_a_2", "completion_b_2"],
|
||||
);
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
@@ -944,7 +944,7 @@ mod tests {
|
||||
});
|
||||
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
@@ -952,7 +952,7 @@ mod tests {
|
||||
three
|
||||
"},
|
||||
vec!["something_else()"],
|
||||
);
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
|
||||
@@ -1320,9 +1320,8 @@ fn render_same_line_diagnostics(
|
||||
let editor_handle = editor_handle.clone();
|
||||
let parent = h_flex()
|
||||
.items_start()
|
||||
.child(v_flex().size_full().when_some_else(
|
||||
toggle_expand_label,
|
||||
|parent, label| {
|
||||
.child(v_flex().size_full().map(|parent| {
|
||||
if let Some(label) = toggle_expand_label {
|
||||
parent.child(Button::new(cx.block_id, label).on_click({
|
||||
let diagnostics = Arc::clone(&diagnostics);
|
||||
move |_, cx| {
|
||||
@@ -1353,16 +1352,15 @@ fn render_same_line_diagnostics(
|
||||
});
|
||||
}
|
||||
}))
|
||||
},
|
||||
|parent| {
|
||||
} else {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.size(IconSize::default().rems())
|
||||
.invisible()
|
||||
.flex_none(),
|
||||
)
|
||||
},
|
||||
));
|
||||
}
|
||||
}));
|
||||
let max_message_rows = if expanded {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -109,6 +109,7 @@ pub struct DisplayMap {
|
||||
crease_map: CreaseMap,
|
||||
fold_placeholder: FoldPlaceholder,
|
||||
pub clip_at_line_ends: bool,
|
||||
pub(crate) masked: bool,
|
||||
}
|
||||
|
||||
impl DisplayMap {
|
||||
@@ -156,6 +157,7 @@ impl DisplayMap {
|
||||
text_highlights: Default::default(),
|
||||
inlay_highlights: Default::default(),
|
||||
clip_at_line_ends: false,
|
||||
masked: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +184,7 @@ impl DisplayMap {
|
||||
text_highlights: self.text_highlights.clone(),
|
||||
inlay_highlights: self.inlay_highlights.clone(),
|
||||
clip_at_line_ends: self.clip_at_line_ends,
|
||||
masked: self.masked,
|
||||
fold_placeholder: self.fold_placeholder.clone(),
|
||||
}
|
||||
}
|
||||
@@ -499,6 +502,7 @@ pub struct DisplaySnapshot {
|
||||
text_highlights: TextHighlights,
|
||||
inlay_highlights: InlayHighlights,
|
||||
clip_at_line_ends: bool,
|
||||
masked: bool,
|
||||
pub(crate) fold_placeholder: FoldPlaceholder,
|
||||
}
|
||||
|
||||
@@ -561,7 +565,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
// used by line_mode selections and tries to match vim behaviour
|
||||
// used by line_mode selections and tries to match vim behavior
|
||||
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||||
let new_start = if range.start.row == 0 {
|
||||
MultiBufferPoint::new(0, 0)
|
||||
@@ -650,6 +654,7 @@ impl DisplaySnapshot {
|
||||
.chunks(
|
||||
display_row.0..self.max_point().row().next_row().0,
|
||||
false,
|
||||
self.masked,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|h| h.text)
|
||||
@@ -657,9 +662,9 @@ impl DisplaySnapshot {
|
||||
|
||||
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||
pub fn reverse_text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
|
||||
(0..=display_row.0).rev().flat_map(|row| {
|
||||
(0..=display_row.0).rev().flat_map(move |row| {
|
||||
self.block_snapshot
|
||||
.chunks(row..row + 1, false, Highlights::default())
|
||||
.chunks(row..row + 1, false, self.masked, Highlights::default())
|
||||
.map(|h| h.text)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
@@ -676,6 +681,7 @@ impl DisplaySnapshot {
|
||||
self.block_snapshot.chunks(
|
||||
display_rows.start.0..display_rows.end.0,
|
||||
language_aware,
|
||||
self.masked,
|
||||
Highlights {
|
||||
text_highlights: Some(&self.text_highlights),
|
||||
inlay_highlights: Some(&self.inlay_highlights),
|
||||
|
||||
@@ -23,6 +23,7 @@ use text::Edit;
|
||||
use ui::ElementId;
|
||||
|
||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||
const BULLETS: &str = "********************************************************************************************************************************";
|
||||
|
||||
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
|
||||
///
|
||||
@@ -285,6 +286,7 @@ pub struct BlockChunks<'a> {
|
||||
input_chunk: Chunk<'a>,
|
||||
output_row: u32,
|
||||
max_output_row: u32,
|
||||
masked: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -893,6 +895,7 @@ impl BlockSnapshot {
|
||||
self.chunks(
|
||||
0..self.transforms.summary().output_rows,
|
||||
false,
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
@@ -903,6 +906,7 @@ impl BlockSnapshot {
|
||||
&'a self,
|
||||
rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
masked: bool,
|
||||
highlights: Highlights<'a>,
|
||||
) -> BlockChunks<'a> {
|
||||
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
|
||||
@@ -941,6 +945,7 @@ impl BlockSnapshot {
|
||||
transforms: cursor,
|
||||
output_row: rows.start,
|
||||
max_output_row,
|
||||
masked,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1229,12 +1234,20 @@ impl<'a> Iterator for BlockChunks<'a> {
|
||||
let (prefix_rows, prefix_bytes) =
|
||||
offset_for_row(self.input_chunk.text, transform_end - self.output_row);
|
||||
self.output_row += prefix_rows;
|
||||
let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
|
||||
let (mut prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
|
||||
self.input_chunk.text = suffix;
|
||||
if self.output_row == transform_end {
|
||||
self.transforms.next(&());
|
||||
}
|
||||
|
||||
if self.masked {
|
||||
// Not great for multibyte text because to keep cursor math correct we
|
||||
// need to have the same number of bytes in the input as output.
|
||||
let chars = prefix.chars().count();
|
||||
let bullet_len = chars;
|
||||
prefix = &BULLETS[..bullet_len];
|
||||
}
|
||||
|
||||
Some(Chunk {
|
||||
text: prefix,
|
||||
..self.input_chunk.clone()
|
||||
@@ -2048,6 +2061,7 @@ mod tests {
|
||||
.chunks(
|
||||
start_row as u32..blocks_snapshot.max_point().row + 1,
|
||||
false,
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
//!
|
||||
//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s).
|
||||
//!
|
||||
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behaviour.
|
||||
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior.
|
||||
pub mod actions;
|
||||
mod blame_entry_tooltip;
|
||||
mod blink_manager;
|
||||
mod debounced_delay;
|
||||
pub mod display_map;
|
||||
mod editor_settings;
|
||||
mod editor_settings_controls;
|
||||
mod element;
|
||||
mod git;
|
||||
mod highlight_matching_bracket;
|
||||
@@ -57,6 +58,7 @@ use debounced_delay::DebouncedDelay;
|
||||
use display_map::*;
|
||||
pub use display_map::{DisplayPoint, FoldPlaceholder};
|
||||
pub use editor_settings::{CurrentLineHighlight, EditorSettings};
|
||||
pub use editor_settings_controls::*;
|
||||
use element::LineWithInvisibles;
|
||||
pub use element::{
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
@@ -406,6 +408,7 @@ impl EditorActionId {
|
||||
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollbarMarkerState {
|
||||
scrollbar_size: Size<Pixels>,
|
||||
dirty: bool,
|
||||
@@ -419,17 +422,6 @@ impl ScrollbarMarkerState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScrollbarMarkerState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scrollbar_size: Size::default(),
|
||||
dirty: false,
|
||||
markers: Arc::from([]),
|
||||
pending_refresh: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RunnableTasks {
|
||||
templates: Vec<(TaskSourceKind, TaskTemplate)>,
|
||||
@@ -488,7 +480,6 @@ pub struct Editor {
|
||||
mode: EditorMode,
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
redact_all: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
@@ -592,7 +583,7 @@ pub struct EditorSnapshot {
|
||||
|
||||
const GIT_BLAME_GUTTER_WIDTH_CHARS: f32 = 53.;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct GutterDimensions {
|
||||
pub left_padding: Pixels,
|
||||
pub right_padding: Pixels,
|
||||
@@ -615,18 +606,6 @@ impl GutterDimensions {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GutterDimensions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
left_padding: Pixels::ZERO,
|
||||
right_padding: Pixels::ZERO,
|
||||
width: Pixels::ZERO,
|
||||
margin: Pixels::ZERO,
|
||||
git_blame_entries_width: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteSelection {
|
||||
pub replica_id: ReplicaId,
|
||||
@@ -1823,7 +1802,6 @@ impl Editor {
|
||||
show_code_actions: None,
|
||||
show_runnables: None,
|
||||
show_wrap_guides: None,
|
||||
redact_all: false,
|
||||
show_indent_guides,
|
||||
placeholder_text: None,
|
||||
highlight_order: 0,
|
||||
@@ -5165,7 +5143,7 @@ impl Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_close_hunk_diff_button(
|
||||
fn close_hunk_diff_button(
|
||||
&self,
|
||||
hunk: HoveredHunk,
|
||||
row: DisplayRow,
|
||||
@@ -5740,7 +5718,7 @@ impl Editor {
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
let empty_str: Arc<str> = "".into();
|
||||
let empty_str: Arc<str> = Arc::default();
|
||||
buffer.edit(
|
||||
deletion_ranges
|
||||
.into_iter()
|
||||
@@ -5806,7 +5784,7 @@ impl Editor {
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
let buffer = this.buffer.update(cx, |buffer, cx| {
|
||||
let empty_str: Arc<str> = "".into();
|
||||
let empty_str: Arc<str> = Arc::default();
|
||||
buffer.edit(
|
||||
edit_ranges
|
||||
.into_iter()
|
||||
@@ -8107,7 +8085,7 @@ impl Editor {
|
||||
let mut selection_edit_ranges = Vec::new();
|
||||
let mut last_toggled_row = None;
|
||||
let snapshot = this.buffer.read(cx).read(cx);
|
||||
let empty_str: Arc<str> = "".into();
|
||||
let empty_str: Arc<str> = Arc::default();
|
||||
let mut suffixes_inserted = Vec::new();
|
||||
|
||||
fn comment_prefix_range(
|
||||
@@ -10440,9 +10418,11 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_redact_all(&mut self, redact_all: bool, cx: &mut ViewContext<Self>) {
|
||||
self.redact_all = redact_all;
|
||||
cx.notify();
|
||||
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
|
||||
if self.display_map.read(cx).masked != masked {
|
||||
self.display_map.update(cx, |map, _| map.masked = masked);
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
|
||||
@@ -10857,17 +10837,6 @@ impl Editor {
|
||||
color_fetcher: fn(&ThemeColors) -> Hsla,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let snapshot = self.snapshot(cx);
|
||||
// this is to try and catch a panic sooner
|
||||
for range in ranges {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.summary_for_anchor::<usize>(&range.start);
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.summary_for_anchor::<usize>(&range.end);
|
||||
}
|
||||
|
||||
self.background_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
@@ -11139,10 +11108,6 @@ impl Editor {
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
cx: &WindowContext,
|
||||
) -> Vec<Range<DisplayPoint>> {
|
||||
if self.redact_all {
|
||||
return vec![DisplayPoint::zero()..display_snapshot.max_point()];
|
||||
}
|
||||
|
||||
display_snapshot
|
||||
.buffer_snapshot
|
||||
.redacted_ranges(search_range, |file| {
|
||||
@@ -11824,8 +11789,24 @@ impl Editor {
|
||||
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);
|
||||
self.display_to_pixel_point(source_point, editor_snapshot, cx)
|
||||
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(
|
||||
@@ -11836,18 +11817,22 @@ impl Editor {
|
||||
) -> 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 scroll_top = text_layout_details
|
||||
let first_visible_line = text_layout_details
|
||||
.scroll_anchor
|
||||
.scroll_position(editor_snapshot)
|
||||
.y;
|
||||
|
||||
if source.row().as_f32() < scroll_top.floor() {
|
||||
.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().as_f32() - scroll_top);
|
||||
let source_y = line_height * (source.row() - first_visible_line.row()).0 as f32;
|
||||
Some(gpui::Point::new(source_x, source_y))
|
||||
}
|
||||
|
||||
fn gutter_bounds(&self) -> Option<Bounds<Pixels>> {
|
||||
let bounds = self.last_bounds?;
|
||||
Some(element::gutter_bounds(bounds, self.gutter_dimensions))
|
||||
}
|
||||
}
|
||||
|
||||
fn hunks_for_selections(
|
||||
@@ -12234,7 +12219,7 @@ impl EditorSnapshot {
|
||||
self.scroll_anchor.scroll_position(&self.display_snapshot)
|
||||
}
|
||||
|
||||
pub fn gutter_dimensions(
|
||||
fn gutter_dimensions(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
font_size: Pixels,
|
||||
@@ -12445,6 +12430,7 @@ impl Render for Editor {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
@@ -12454,6 +12440,7 @@ impl Render for Editor {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
|
||||
@@ -305,7 +305,7 @@ pub struct ScrollbarContent {
|
||||
}
|
||||
|
||||
/// Gutter related settings
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct GutterContent {
|
||||
/// Whether to show line numbers in the gutter.
|
||||
///
|
||||
|
||||
427
crates/editor/src/editor_settings_controls.rs
Normal file
427
crates/editor/src/editor_settings_controls.rs
Normal file
@@ -0,0 +1,427 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, FontFeatures, FontWeight};
|
||||
use project::project_settings::{InlineBlameSettings, ProjectSettings};
|
||||
use settings::{EditableSettingControl, Settings};
|
||||
use theme::{FontFamilyCache, ThemeSettings};
|
||||
use ui::{
|
||||
prelude::*, CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer,
|
||||
SettingsGroup,
|
||||
};
|
||||
|
||||
use crate::EditorSettings;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct EditorSettingsControls {}
|
||||
|
||||
impl EditorSettingsControls {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for EditorSettingsControls {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
SettingsContainer::new()
|
||||
.child(
|
||||
SettingsGroup::new("Font")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(BufferFontFamilyControl)
|
||||
.child(BufferFontWeightControl),
|
||||
)
|
||||
.child(BufferFontSizeControl)
|
||||
.child(BufferFontLigaturesControl),
|
||||
)
|
||||
.child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
|
||||
.child(
|
||||
SettingsGroup::new("Gutter").child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(LineNumbersControl)
|
||||
.child(RelativeLineNumbersControl),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct BufferFontFamilyControl;
|
||||
|
||||
impl EditableSettingControl for BufferFontFamilyControl {
|
||||
type Value = SharedString;
|
||||
type Settings = ThemeSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Buffer Font Family".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings.buffer_font.family.clone()
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
settings.buffer_font_family = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BufferFontFamilyControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Font))
|
||||
.child(DropdownMenu::new(
|
||||
"buffer-font-family",
|
||||
value.clone(),
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
|
||||
for font_name in font_family_cache.list_font_families(cx) {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_cx| Label::new(font_name.clone()).into_any_element()
|
||||
},
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |cx| {
|
||||
Self::write(font_name.clone(), cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
menu
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct BufferFontSizeControl;
|
||||
|
||||
impl EditableSettingControl for BufferFontSizeControl {
|
||||
type Value = Pixels;
|
||||
type Settings = ThemeSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Buffer Font Size".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings.buffer_font_size
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
settings.buffer_font_size = Some(value.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BufferFontSizeControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::FontSize))
|
||||
.child(NumericStepper::new(
|
||||
value.to_string(),
|
||||
move |_, cx| {
|
||||
Self::write(value - px(1.), cx);
|
||||
},
|
||||
move |_, cx| {
|
||||
Self::write(value + px(1.), cx);
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct BufferFontWeightControl;
|
||||
|
||||
impl EditableSettingControl for BufferFontWeightControl {
|
||||
type Value = FontWeight;
|
||||
type Settings = ThemeSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Buffer Font Weight".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings.buffer_font.weight
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
settings.buffer_font_weight = Some(value.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BufferFontWeightControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::FontWeight))
|
||||
.child(DropdownMenu::new(
|
||||
"buffer-font-weight",
|
||||
value.0.to_string(),
|
||||
ContextMenu::build(cx, |mut menu, _cx| {
|
||||
for weight in FontWeight::ALL {
|
||||
menu = menu.custom_entry(
|
||||
move |_cx| Label::new(weight.0.to_string()).into_any_element(),
|
||||
{
|
||||
move |cx| {
|
||||
Self::write(weight, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
menu
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct BufferFontLigaturesControl;
|
||||
|
||||
impl EditableSettingControl for BufferFontLigaturesControl {
|
||||
type Value = bool;
|
||||
type Settings = ThemeSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Buffer Font Ligatures".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings
|
||||
.buffer_font
|
||||
.features
|
||||
.is_calt_enabled()
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
let value = if value { 1 } else { 0 };
|
||||
|
||||
let mut features = settings
|
||||
.buffer_font_features
|
||||
.as_ref()
|
||||
.map(|features| {
|
||||
features
|
||||
.tag_value_list()
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
|
||||
features[calt_index].1 = value;
|
||||
} else {
|
||||
features.push(("calt".into(), value));
|
||||
}
|
||||
|
||||
settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BufferFontLigaturesControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
CheckboxWithLabel::new(
|
||||
"buffer-font-ligatures",
|
||||
Label::new(self.name()),
|
||||
value.into(),
|
||||
|selection, cx| {
|
||||
Self::write(
|
||||
match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct InlineGitBlameControl;
|
||||
|
||||
impl EditableSettingControl for InlineGitBlameControl {
|
||||
type Value = bool;
|
||||
type Settings = ProjectSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Inline Git Blame".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ProjectSettings::get_global(cx);
|
||||
settings.git.inline_blame_enabled()
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
if let Some(inline_blame) = settings.git.inline_blame.as_mut() {
|
||||
inline_blame.enabled = value;
|
||||
} else {
|
||||
settings.git.inline_blame = Some(InlineBlameSettings {
|
||||
enabled: false,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for InlineGitBlameControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
CheckboxWithLabel::new(
|
||||
"inline-git-blame",
|
||||
Label::new(self.name()),
|
||||
value.into(),
|
||||
|selection, cx| {
|
||||
Self::write(
|
||||
match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct LineNumbersControl;
|
||||
|
||||
impl EditableSettingControl for LineNumbersControl {
|
||||
type Value = bool;
|
||||
type Settings = EditorSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Line Numbers".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = EditorSettings::get_global(cx);
|
||||
settings.gutter.line_numbers
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
if let Some(gutter) = settings.gutter.as_mut() {
|
||||
gutter.line_numbers = Some(value);
|
||||
} else {
|
||||
settings.gutter = Some(crate::editor_settings::GutterContent {
|
||||
line_numbers: Some(value),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LineNumbersControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
CheckboxWithLabel::new(
|
||||
"line-numbers",
|
||||
Label::new(self.name()),
|
||||
value.into(),
|
||||
|selection, cx| {
|
||||
Self::write(
|
||||
match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct RelativeLineNumbersControl;
|
||||
|
||||
impl EditableSettingControl for RelativeLineNumbersControl {
|
||||
type Value = bool;
|
||||
type Settings = EditorSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Relative Line Numbers".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = EditorSettings::get_global(cx);
|
||||
settings.relative_line_numbers
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
settings.relative_line_numbers = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for RelativeLineNumbersControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
DropdownMenu::new(
|
||||
"relative-line-numbers",
|
||||
if value { "Relative" } else { "Ascending" },
|
||||
ContextMenu::build(cx, |menu, _cx| {
|
||||
menu.custom_entry(
|
||||
|_cx| Label::new("Ascending").into_any_element(),
|
||||
move |cx| Self::write(false, cx),
|
||||
)
|
||||
.custom_entry(
|
||||
|_cx| Label::new("Relative").into_any_element(),
|
||||
move |cx| Self::write(true, cx),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4716,12 +4716,13 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
|
||||
view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
editor
|
||||
.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
|
||||
@@ -4731,94 +4732,126 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 23)..DisplayPoint::new(DisplayRow(0), 27),
|
||||
DisplayPoint::new(DisplayRow(2), 35)..DisplayPoint::new(DisplayRow(2), 7),
|
||||
DisplayPoint::new(DisplayRow(3), 15)..DisplayPoint::new(DisplayRow(3), 21),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, «mod4ˇ»};
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
fn fn_1«ˇ(param1: bool, param2: &str)» {
|
||||
let var1 = "«textˇ»";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::«{mod3, mod4}ˇ»;
|
||||
|
||||
«ˇfn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "text";
|
||||
}»
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 28),
|
||||
DisplayPoint::new(DisplayRow(4), 1)..DisplayPoint::new(DisplayRow(2), 0),
|
||||
]
|
||||
);
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
|
||||
// Trying to expand the selected syntax node one more time has no effect.
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 28),
|
||||
DisplayPoint::new(DisplayRow(4), 1)..DisplayPoint::new(DisplayRow(2), 0),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::«{mod3, mod4}ˇ»;
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
«ˇfn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "text";
|
||||
}»
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 23)..DisplayPoint::new(DisplayRow(0), 27),
|
||||
DisplayPoint::new(DisplayRow(2), 35)..DisplayPoint::new(DisplayRow(2), 7),
|
||||
DisplayPoint::new(DisplayRow(3), 15)..DisplayPoint::new(DisplayRow(3), 21),
|
||||
]
|
||||
);
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
|
||||
DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
|
||||
DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, «mod4ˇ»};
|
||||
|
||||
fn fn_1«ˇ(param1: bool, param2: &str)» {
|
||||
let var1 = "«textˇ»";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mo«ˇ»d4};
|
||||
|
||||
fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
|
||||
let var1 = "te«ˇ»xt";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Trying to shrink the selected syntax node one more time has no effect.
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
|
||||
DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
|
||||
DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mo«ˇ»d4};
|
||||
|
||||
fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
|
||||
let var1 = "te«ˇ»xt";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Ensure that we keep expanding the selection if the larger selection starts or ends within
|
||||
// a fold.
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
(
|
||||
@@ -4835,14 +4868,19 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 28),
|
||||
DisplayPoint::new(DisplayRow(2), 35)..DisplayPoint::new(DisplayRow(2), 7),
|
||||
DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(3), 23),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::«{mod3, mod4}ˇ»;
|
||||
|
||||
fn fn_1«ˇ(param1: bool, param2: &str)» {
|
||||
«let var1 = "text";ˇ»
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -8173,11 +8211,13 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
|
||||
// TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
|
||||
// Uncommenting and commenting from this position brings in even more wrong artifacts.
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
<!-- ˇ<script> -->
|
||||
// ˇvar x = new Y();
|
||||
<!-- ˇ</script> -->
|
||||
// ˇ</script>
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
@@ -10065,7 +10105,7 @@ struct Row8;
|
||||
struct Row9;
|
||||
struct Row10;"#};
|
||||
|
||||
// Deletion hunks trigger with carets on ajacent rows, so carets and selections have to stay farther to avoid the revert
|
||||
// Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row2;
|
||||
|
||||
@@ -59,6 +59,7 @@ use std::{
|
||||
fmt::{self, Write},
|
||||
iter, mem,
|
||||
ops::{Deref, Range},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
@@ -1248,7 +1249,7 @@ impl EditorElement {
|
||||
|
||||
// Folds contained in a hunk are ignored apart from shrinking visual size
|
||||
// If a fold contains any hunks then that fold line is marked as modified
|
||||
fn layout_git_gutters(
|
||||
fn layout_gutter_git_hunks(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
gutter_hitbox: &Hitbox,
|
||||
@@ -1553,12 +1554,14 @@ impl EditorElement {
|
||||
(offset_y, length)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
@@ -1602,6 +1605,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
Some(button)
|
||||
@@ -1610,6 +1614,7 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_code_actions_indicator(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -1617,6 +1622,7 @@ impl EditorElement {
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
let mut active = false;
|
||||
@@ -1640,6 +1646,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -1963,6 +1970,7 @@ impl EditorElement {
|
||||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||||
editor_style: &self.style,
|
||||
}))
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -3170,7 +3178,7 @@ impl EditorElement {
|
||||
});
|
||||
}
|
||||
|
||||
fn diff_hunk_bounds(
|
||||
pub(super) fn diff_hunk_bounds(
|
||||
snapshot: &EditorSnapshot,
|
||||
line_height: Pixels,
|
||||
gutter_bounds: Bounds<Pixels>,
|
||||
@@ -4040,20 +4048,22 @@ impl EditorElement {
|
||||
self.column_pixels(digit_count, cx)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_hunk_diff_close_indicators(
|
||||
&self,
|
||||
expanded_hunks_by_rows: HashMap<DisplayRow, ExpandedHunk>,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
expanded_hunks_by_rows: HashMap<DisplayRow, ExpandedHunk>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
expanded_hunks_by_rows
|
||||
.into_iter()
|
||||
.map(|(display_row, hunk)| {
|
||||
let button = editor.render_close_hunk_diff_button(
|
||||
let button = editor.close_hunk_diff_button(
|
||||
HoveredHunk {
|
||||
multi_buffer_range: hunk.hunk_range,
|
||||
status: hunk.status,
|
||||
@@ -4070,6 +4080,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -4078,6 +4089,7 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
row: DisplayRow,
|
||||
@@ -4085,6 +4097,7 @@ fn prepaint_gutter_button(
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> AnyElement {
|
||||
let mut button = button.into_any_element();
|
||||
@@ -4094,14 +4107,16 @@ fn prepaint_gutter_button(
|
||||
);
|
||||
let indicator_size = button.layout_as_root(available_space, cx);
|
||||
|
||||
let blame_width = gutter_dimensions
|
||||
.git_blame_entries_width
|
||||
.unwrap_or(Pixels::ZERO);
|
||||
let blame_width = gutter_dimensions.git_blame_entries_width;
|
||||
let gutter_width = rows_with_hunk_bounds
|
||||
.get(&row)
|
||||
.map(|bounds| bounds.size.width);
|
||||
let left_offset = blame_width.max(gutter_width).unwrap_or_default();
|
||||
|
||||
let mut x = blame_width;
|
||||
let mut x = left_offset;
|
||||
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
||||
- indicator_size.width
|
||||
- blame_width;
|
||||
- left_offset;
|
||||
x += available_width / 2.;
|
||||
|
||||
let mut y = row.as_f32() * line_height - scroll_pixel_position.y;
|
||||
@@ -4949,13 +4964,8 @@ impl Element for EditorElement {
|
||||
.collect::<SmallVec<[_; 2]>>();
|
||||
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
let gutter_hitbox = cx.insert_hitbox(
|
||||
Bounds {
|
||||
origin: bounds.origin,
|
||||
size: size(gutter_dimensions.width, bounds.size.height),
|
||||
},
|
||||
false,
|
||||
);
|
||||
let gutter_hitbox =
|
||||
cx.insert_hitbox(gutter_bounds(bounds, gutter_dimensions), false);
|
||||
let text_hitbox = cx.insert_hitbox(
|
||||
Bounds {
|
||||
origin: gutter_hitbox.upper_right(),
|
||||
@@ -5078,7 +5088,7 @@ impl Element for EditorElement {
|
||||
self.layout_crease_trailers(buffer_rows.iter().copied(), &snapshot, cx)
|
||||
});
|
||||
|
||||
let display_hunks = self.layout_git_gutters(
|
||||
let display_hunks = self.layout_gutter_git_hunks(
|
||||
line_height,
|
||||
&gutter_hitbox,
|
||||
start_row..end_row,
|
||||
@@ -5305,6 +5315,27 @@ impl Element for EditorElement {
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
|
||||
let rows_with_hunk_bounds = display_hunks
|
||||
.iter()
|
||||
.filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds)))
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
|mut rows_with_hunk_bounds, (hunk, bounds)| {
|
||||
match hunk {
|
||||
DisplayDiffHunk::Folded { display_row } => {
|
||||
rows_with_hunk_bounds.insert(*display_row, bounds);
|
||||
}
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => {
|
||||
for display_row in display_row_range.iter_rows() {
|
||||
rows_with_hunk_bounds.insert(display_row, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
rows_with_hunk_bounds
|
||||
},
|
||||
);
|
||||
let mut _context_menu_visible = false;
|
||||
let mut code_actions_indicator = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
@@ -5353,6 +5384,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -5368,6 +5400,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&snapshot,
|
||||
cx,
|
||||
)
|
||||
@@ -5376,11 +5409,12 @@ impl Element for EditorElement {
|
||||
};
|
||||
|
||||
let close_indicators = self.layout_hunk_diff_close_indicators(
|
||||
expanded_add_hunks_by_rows,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
expanded_add_hunks_by_rows,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -5460,7 +5494,7 @@ impl Element for EditorElement {
|
||||
|
||||
EditorLayout {
|
||||
mode: snapshot.mode,
|
||||
position_map: Arc::new(PositionMap {
|
||||
position_map: Rc::new(PositionMap {
|
||||
size: bounds.size,
|
||||
scroll_pixel_position,
|
||||
scroll_max,
|
||||
@@ -5591,6 +5625,16 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn gutter_bounds(
|
||||
editor_bounds: Bounds<Pixels>,
|
||||
gutter_dimensions: GutterDimensions,
|
||||
) -> Bounds<Pixels> {
|
||||
Bounds {
|
||||
origin: editor_bounds.origin,
|
||||
size: size(gutter_dimensions.width, editor_bounds.size.height),
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for EditorElement {
|
||||
type Element = Self;
|
||||
|
||||
@@ -5600,7 +5644,7 @@ impl IntoElement for EditorElement {
|
||||
}
|
||||
|
||||
pub struct EditorLayout {
|
||||
position_map: Arc<PositionMap>,
|
||||
position_map: Rc<PositionMap>,
|
||||
hitbox: Hitbox,
|
||||
text_hitbox: Hitbox,
|
||||
gutter_hitbox: Hitbox,
|
||||
|
||||
@@ -786,6 +786,7 @@ mod tests {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use gpui::{Action, AppContext, Hsla, Model, MouseButton, Subscription, Task, View};
|
||||
use gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View};
|
||||
use language::Buffer;
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
|
||||
@@ -13,8 +13,8 @@ use multi_buffer::{
|
||||
use settings::SettingsStore;
|
||||
use text::{BufferId, Point};
|
||||
use ui::{
|
||||
h_flex, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, IntoElement,
|
||||
ParentElement, Pixels, Styled, ViewContext, VisualContext,
|
||||
div, h_flex, rems, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Styled, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{debug_panic, RangeExt};
|
||||
|
||||
@@ -24,8 +24,8 @@ use crate::{
|
||||
hunk_status, hunks_for_selections,
|
||||
mouse_context_menu::MouseContextMenu,
|
||||
BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, Editor,
|
||||
EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks, ToDisplayPoint,
|
||||
ToggleHunkDiff,
|
||||
EditorElement, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks,
|
||||
ToDisplayPoint, ToggleHunkDiff,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -430,7 +430,6 @@ impl Editor {
|
||||
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 {
|
||||
@@ -439,37 +438,87 @@ impl Editor {
|
||||
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;
|
||||
let Some(gutter_bounds) = editor.read(cx).gutter_bounds() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
let (gutter_dimensions, hunk_bounds, close_button) =
|
||||
editor.update(cx.context, |editor, cx| {
|
||||
let editor_snapshot = editor.snapshot(cx);
|
||||
let hunk_display_range = hunk
|
||||
.multi_buffer_range
|
||||
.clone()
|
||||
.to_display_points(&editor_snapshot);
|
||||
let gutter_dimensions = editor.gutter_dimensions;
|
||||
let hunk_bounds = EditorElement::diff_hunk_bounds(
|
||||
&editor_snapshot,
|
||||
cx.line_height(),
|
||||
gutter_bounds,
|
||||
&DisplayDiffHunk::Unfolded {
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
multi_buffer_range: hunk.multi_buffer_range.clone(),
|
||||
display_row_range: hunk_display_range.start.row()
|
||||
..hunk_display_range.end.row(),
|
||||
status: hunk.status,
|
||||
},
|
||||
);
|
||||
|
||||
let close_button = editor.close_hunk_diff_button(
|
||||
hunk.clone(),
|
||||
hunk_display_range.start.row(),
|
||||
cx,
|
||||
);
|
||||
(gutter_dimensions, hunk_bounds, close_button)
|
||||
});
|
||||
let click_editor = editor.clone();
|
||||
let clicked_hunk = hunk.clone();
|
||||
h_flex()
|
||||
.id("gutter with editor")
|
||||
.bg(deleted_hunk_color)
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex()
|
||||
h_flex()
|
||||
.id("gutter")
|
||||
.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(
|
||||
h_flex()
|
||||
.id("gutter hunk")
|
||||
.pl(gutter_dimensions.margin
|
||||
+ gutter_dimensions
|
||||
.git_blame_entries_width
|
||||
.unwrap_or_default())
|
||||
.max_w(hunk_bounds.size.width)
|
||||
.min_w(hunk_bounds.size.width)
|
||||
.size_full()
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.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);
|
||||
});
|
||||
} else {
|
||||
click_editor.update(cx, |editor, cx| {
|
||||
editor.open_hunk_context_menu(
|
||||
clicked_hunk.clone(),
|
||||
e.position,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.pt(rems(0.25))
|
||||
.justify_start()
|
||||
.child(close_button),
|
||||
),
|
||||
)
|
||||
.child(editor_with_deleted_text.clone())
|
||||
.into_any_element()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::{
|
||||
editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
|
||||
Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
|
||||
NavigationData, SearchWithinRange, ToPoint as _,
|
||||
editor_settings::SeedQuerySetting,
|
||||
persistence::{SerializedEditor, DB},
|
||||
scroll::ScrollAnchor,
|
||||
Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer,
|
||||
MultiBufferSnapshot, NavigationData, SearchWithinRange, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
@@ -914,18 +916,23 @@ impl SerializableItem for Editor {
|
||||
item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let path_content_language = match DB
|
||||
.get_path_and_contents(item_id, workspace_id)
|
||||
let serialized_editor = match DB
|
||||
.get_serialized_editor(item_id, workspace_id)
|
||||
.context("Failed to query editor state")
|
||||
{
|
||||
Ok(Some((path, content, language))) => {
|
||||
Ok(Some(serialized_editor)) => {
|
||||
if ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers
|
||||
{
|
||||
(path, content, language)
|
||||
serialized_editor
|
||||
} else {
|
||||
(path, None, None)
|
||||
SerializedEditor {
|
||||
path: serialized_editor.path,
|
||||
contents: None,
|
||||
language: None,
|
||||
mtime: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
@@ -936,34 +943,48 @@ impl SerializableItem for Editor {
|
||||
}
|
||||
};
|
||||
|
||||
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())?;
|
||||
let buffer_task = match serialized_editor {
|
||||
SerializedEditor {
|
||||
path: None,
|
||||
contents: Some(contents),
|
||||
language,
|
||||
..
|
||||
} => cx.spawn(|_, mut cx| {
|
||||
let project = project.clone();
|
||||
async move {
|
||||
let language = if let Some(language_name) = language {
|
||||
let language_registry =
|
||||
project.update(&mut cx, |project, _| project.languages().clone())?;
|
||||
|
||||
Some(language_registry.language_for_name(&language_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// We don't fail here, because we'd rather not set the language if the name changed
|
||||
// than fail to restore the buffer.
|
||||
language_registry
|
||||
.language_for_name(&language_name)
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// First create the empty buffer
|
||||
let buffer = project.update(&mut cx, |project, cx| {
|
||||
project.create_local_buffer("", language, cx)
|
||||
})?;
|
||||
// First create the empty buffer
|
||||
let buffer = project.update(&mut cx, |project, cx| {
|
||||
project.create_local_buffer("", language, cx)
|
||||
})?;
|
||||
|
||||
// Then set the text so that the dirty bit is set correctly
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_text(content, cx);
|
||||
})?;
|
||||
// Then set the text so that the dirty bit is set correctly
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_text(contents, 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
|
||||
})
|
||||
anyhow::Ok(buffer)
|
||||
}
|
||||
}),
|
||||
(Some(path), contents, _) => {
|
||||
SerializedEditor {
|
||||
path: Some(path),
|
||||
contents,
|
||||
mtime,
|
||||
..
|
||||
} => {
|
||||
let project_item = project.update(cx, |project, cx| {
|
||||
let (worktree, path) = project
|
||||
.find_worktree(&path, cx)
|
||||
@@ -978,7 +999,7 @@ impl SerializableItem for Editor {
|
||||
|
||||
project_item
|
||||
.map(|project_item| {
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
cx.spawn(|_, 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")
|
||||
@@ -988,27 +1009,43 @@ impl SerializableItem for Editor {
|
||||
// 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, ...).
|
||||
// by loading the file (git diff base, ...).
|
||||
if let Some(buffer_text) = contents {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
// If we did restore an mtime, we want to store it on the buffer
|
||||
// so that the next edit will mark the buffer as dirty/conflicted.
|
||||
if mtime.is_some() {
|
||||
buffer.did_reload(
|
||||
buffer.version(),
|
||||
buffer.line_ending(),
|
||||
mtime,
|
||||
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
|
||||
})
|
||||
})
|
||||
Ok(buffer)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
_ => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
|
||||
}
|
||||
_ => return Task::ready(Err(anyhow!("No path or contents found for buffer"))),
|
||||
};
|
||||
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
let buffer = buffer_task.await?;
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(
|
||||
@@ -1036,36 +1073,33 @@ impl SerializableItem for Editor {
|
||||
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 local_file = buffer.read(cx).file().and_then(|file| file.as_local());
|
||||
let path = local_file.map(|file| file.abs_path(cx));
|
||||
let mtime = buffer.read(cx).saved_mtime();
|
||||
|
||||
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")?
|
||||
}
|
||||
let (contents, language) = if serialize_dirty_buffers && is_dirty {
|
||||
let contents = snapshot.text();
|
||||
let language = snapshot.language().map(|lang| lang.name().to_string());
|
||||
(Some(contents), language)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
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)
|
||||
};
|
||||
let editor = SerializedEditor {
|
||||
path,
|
||||
contents,
|
||||
language,
|
||||
mtime,
|
||||
};
|
||||
|
||||
DB.save_contents(item_id, workspace_id, contents, language)
|
||||
.await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
DB.save_serialized_editor(item_id, workspace_id, editor)
|
||||
.await
|
||||
.context("failed to save serialized editor")
|
||||
})
|
||||
.await
|
||||
.context("failed to save contents of buffer")?;
|
||||
@@ -1474,10 +1508,16 @@ fn path_for_file<'a>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::editor_tests::init_test;
|
||||
|
||||
use super::*;
|
||||
use gpui::AppContext;
|
||||
use language::TestFile;
|
||||
use std::path::Path;
|
||||
use gpui::{AppContext, VisualTestContext};
|
||||
use language::{LanguageMatcher, TestFile};
|
||||
use project::FakeFs;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_path_for_file(cx: &mut AppContext) {
|
||||
@@ -1487,4 +1527,183 @@ mod tests {
|
||||
};
|
||||
assert_eq!(path_for_file(&file, 0, false, cx), None);
|
||||
}
|
||||
|
||||
async fn deserialize_editor(
|
||||
item_id: ItemId,
|
||||
workspace_id: WorkspaceId,
|
||||
workspace: View<Workspace>,
|
||||
project: Model<Project>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> View<Editor> {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let pane = workspace.active_pane();
|
||||
pane.update(cx, |_, cx| {
|
||||
Editor::deserialize(
|
||||
project.clone(),
|
||||
workspace.weak_handle(),
|
||||
workspace_id,
|
||||
item_id,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn rust_language() -> Arc<language::Language> {
|
||||
Arc::new(language::Language::new(
|
||||
language::LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let now = SystemTime::now();
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.set_next_mtime(now);
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
|
||||
// Test case 1: Deserialize with path and contents
|
||||
{
|
||||
let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
let item_id = 1234 as ItemId;
|
||||
|
||||
let serialized_editor = SerializedEditor {
|
||||
path: Some(PathBuf::from("/file.rs")),
|
||||
contents: Some("fn main() {}".to_string()),
|
||||
language: Some("Rust".to_string()),
|
||||
mtime: Some(now),
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deserialized =
|
||||
deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
|
||||
|
||||
deserialized.update(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() {}");
|
||||
assert!(editor.is_dirty(cx));
|
||||
assert!(!editor.has_conflict(cx));
|
||||
let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
assert!(buffer.file().is_some());
|
||||
});
|
||||
}
|
||||
|
||||
// Test case 2: Deserialize with only path
|
||||
{
|
||||
let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
|
||||
let item_id = 5678 as ItemId;
|
||||
let serialized_editor = SerializedEditor {
|
||||
path: Some(PathBuf::from("/file.rs")),
|
||||
contents: None,
|
||||
language: None,
|
||||
mtime: None,
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deserialized =
|
||||
deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
|
||||
|
||||
deserialized.update(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), ""); // The file should be empty as per our initial setup
|
||||
assert!(!editor.is_dirty(cx));
|
||||
assert!(!editor.has_conflict(cx));
|
||||
|
||||
let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
assert!(buffer.file().is_some());
|
||||
});
|
||||
}
|
||||
|
||||
// Test case 3: Deserialize with no path (untitled buffer, with content and language)
|
||||
{
|
||||
let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
|
||||
// Add Rust to the language, so that we can restore the language of the buffer
|
||||
project.update(cx, |project, _| project.languages().add(rust_language()));
|
||||
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
|
||||
let item_id = 9012 as ItemId;
|
||||
let serialized_editor = SerializedEditor {
|
||||
path: None,
|
||||
contents: Some("hello".to_string()),
|
||||
language: Some("Rust".to_string()),
|
||||
mtime: None,
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deserialized =
|
||||
deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
|
||||
|
||||
deserialized.update(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "hello");
|
||||
assert!(editor.is_dirty(cx)); // The editor should be dirty for an untitled buffer
|
||||
|
||||
let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
buffer.language().map(|lang| lang.name()).as_deref(),
|
||||
Some("Rust")
|
||||
); // Language should be set to Rust
|
||||
assert!(buffer.file().is_none()); // The buffer should not have an associated file
|
||||
});
|
||||
}
|
||||
|
||||
// Test case 4: Deserialize with path, content, and old mtime
|
||||
{
|
||||
let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
|
||||
let item_id = 9345 as ItemId;
|
||||
let old_mtime = now
|
||||
.checked_sub(std::time::Duration::from_secs(60 * 60 * 24))
|
||||
.unwrap();
|
||||
let serialized_editor = SerializedEditor {
|
||||
path: Some(PathBuf::from("/file.rs")),
|
||||
contents: Some("fn main() {}".to_string()),
|
||||
language: Some("Rust".to_string()),
|
||||
mtime: Some(old_mtime),
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deserialized =
|
||||
deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
|
||||
|
||||
deserialized.update(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() {}");
|
||||
assert!(editor.has_conflict(cx)); // The editor should have a conflict
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use gpui::prelude::FluentBuilder;
|
||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||
use workspace::OpenInTerminal;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MenuPosition {
|
||||
/// When the editor is scrolled, the context menu stays on the exact
|
||||
/// same position on the screen, never disappearing.
|
||||
@@ -30,15 +29,6 @@ pub struct MouseContextMenu {
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MouseContextMenu {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MouseContextMenu")
|
||||
.field("position", &self.position)
|
||||
.field("context_menu", &self.context_menu)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseContextMenu {
|
||||
pub(crate) fn pinned_to_editor(
|
||||
editor: &mut Editor,
|
||||
|
||||
@@ -1,12 +1,80 @@
|
||||
use anyhow::Result;
|
||||
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||
use db::sqlez::statement::Statement;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub(crate) struct SerializedEditor {
|
||||
pub(crate) path: Option<PathBuf>,
|
||||
pub(crate) contents: Option<String>,
|
||||
pub(crate) language: Option<String>,
|
||||
pub(crate) mtime: Option<SystemTime>,
|
||||
}
|
||||
|
||||
impl StaticColumnCount for SerializedEditor {
|
||||
fn column_count() -> usize {
|
||||
5
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for SerializedEditor {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let start_index = statement.bind(&self.path, start_index)?;
|
||||
let start_index = statement.bind(&self.contents, start_index)?;
|
||||
let start_index = statement.bind(&self.language, start_index)?;
|
||||
|
||||
let mtime = self.mtime.and_then(|mtime| {
|
||||
mtime
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| (duration.as_secs() as i64, duration.subsec_nanos() as i32))
|
||||
});
|
||||
let start_index = match mtime {
|
||||
Some((seconds, nanos)) => {
|
||||
let start_index = statement.bind(&seconds, start_index)?;
|
||||
statement.bind(&nanos, start_index)?
|
||||
}
|
||||
None => {
|
||||
let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
|
||||
statement.bind::<Option<i32>>(&None, start_index)?
|
||||
}
|
||||
};
|
||||
Ok(start_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for SerializedEditor {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (path, start_index): (Option<PathBuf>, i32) = Column::column(statement, start_index)?;
|
||||
let (contents, start_index): (Option<String>, i32) =
|
||||
Column::column(statement, start_index)?;
|
||||
let (language, start_index): (Option<String>, i32) =
|
||||
Column::column(statement, start_index)?;
|
||||
let (mtime_seconds, start_index): (Option<i64>, i32) =
|
||||
Column::column(statement, start_index)?;
|
||||
let (mtime_nanos, start_index): (Option<i32>, i32) =
|
||||
Column::column(statement, start_index)?;
|
||||
|
||||
let mtime = mtime_seconds
|
||||
.zip(mtime_nanos)
|
||||
.map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32));
|
||||
|
||||
let editor = Self {
|
||||
path,
|
||||
contents,
|
||||
language,
|
||||
mtime,
|
||||
};
|
||||
Ok((editor, start_index))
|
||||
}
|
||||
}
|
||||
|
||||
define_connection!(
|
||||
// Current schema shape using pseudo-rust syntax:
|
||||
// editors(
|
||||
@@ -18,6 +86,8 @@ define_connection!(
|
||||
// scroll_horizontal_offset: f32,
|
||||
// content: Option<String>,
|
||||
// language: Option<String>,
|
||||
// mtime_seconds: Option<i64>,
|
||||
// mtime_nanos: Option<i32>,
|
||||
// )
|
||||
pub static ref DB: EditorDb<WorkspaceDb> =
|
||||
&[sql! (
|
||||
@@ -61,41 +131,36 @@ define_connection!(
|
||||
DROP TABLE editors;
|
||||
|
||||
ALTER TABLE new_editors_tmp RENAME TO editors;
|
||||
)];
|
||||
),
|
||||
sql! (
|
||||
ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
|
||||
ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
|
||||
),
|
||||
];
|
||||
);
|
||||
|
||||
impl EditorDb {
|
||||
query! {
|
||||
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
|
||||
pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
|
||||
SELECT path, contents, language, mtime_seconds, mtime_nanos FROM editors
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
|
||||
pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
|
||||
INSERT INTO editors
|
||||
(item_id, workspace_id, path)
|
||||
(item_id, workspace_id, path, contents, language, mtime_seconds, mtime_nanos)
|
||||
VALUES
|
||||
(?1, ?2, ?3)
|
||||
(?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
ON CONFLICT DO UPDATE SET
|
||||
item_id = ?1,
|
||||
workspace_id = ?2,
|
||||
path = ?3
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
path = ?3,
|
||||
contents = ?4,
|
||||
language = ?5,
|
||||
mtime_seconds = ?6,
|
||||
mtime_nanos = ?7
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,41 +223,79 @@ mod tests {
|
||||
use gpui;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_saving_content() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
async fn test_save_and_get_serialized_editor() {
|
||||
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);
|
||||
let serialized_editor = SerializedEditor {
|
||||
path: Some(PathBuf::from("testing.txt")),
|
||||
contents: None,
|
||||
language: None,
|
||||
mtime: 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)
|
||||
DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
|
||||
.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());
|
||||
let have = DB
|
||||
.get_serialized_editor(1234, workspace_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(have, serialized_editor);
|
||||
|
||||
// Now update contents and language
|
||||
let serialized_editor = SerializedEditor {
|
||||
path: Some(PathBuf::from("testing.txt")),
|
||||
contents: Some("Test".to_owned()),
|
||||
language: Some("Go".to_owned()),
|
||||
mtime: None,
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let have = DB
|
||||
.get_serialized_editor(1234, workspace_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(have, serialized_editor);
|
||||
|
||||
// Now set all the fields to NULL
|
||||
let serialized_editor = SerializedEditor {
|
||||
path: None,
|
||||
contents: None,
|
||||
language: None,
|
||||
mtime: None,
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let have = DB
|
||||
.get_serialized_editor(1234, workspace_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(have, serialized_editor);
|
||||
|
||||
// Storing and retrieving mtime
|
||||
let now = SystemTime::now();
|
||||
let serialized_editor = SerializedEditor {
|
||||
path: None,
|
||||
contents: None,
|
||||
language: None,
|
||||
mtime: Some(now),
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let have = DB
|
||||
.get_serialized_editor(1234, workspace_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(have, serialized_editor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ impl SelectionsCollection {
|
||||
buffer,
|
||||
next_selection_id: 1,
|
||||
line_mode: false,
|
||||
disjoint: Arc::from([]),
|
||||
disjoint: Arc::default(),
|
||||
pending: Some(PendingSelection {
|
||||
selection: Selection {
|
||||
id: 0,
|
||||
@@ -398,7 +398,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
}
|
||||
|
||||
pub fn clear_disjoint(&mut self) {
|
||||
self.collection.disjoint = Arc::from([]);
|
||||
self.collection.disjoint = Arc::default();
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, selection_id: usize) {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::Editor;
|
||||
use gpui::{Task as AsyncTask, WindowContext};
|
||||
use project::Location;
|
||||
use task::{TaskContext, TaskVariables, VariableName};
|
||||
use text::{Point, ToOffset, ToPoint};
|
||||
use text::{ToOffset, ToPoint};
|
||||
use workspace::Workspace;
|
||||
|
||||
fn task_context_with_editor(
|
||||
@@ -14,11 +14,7 @@ fn task_context_with_editor(
|
||||
return AsyncTask::ready(None);
|
||||
};
|
||||
let (selection, buffer, editor_snapshot) = {
|
||||
let mut selection = editor.selections.newest::<Point>(cx);
|
||||
if editor.selections.line_mode {
|
||||
selection.start = Point::new(selection.start.row, 0);
|
||||
selection.end = Point::new(selection.end.row + 1, 0);
|
||||
}
|
||||
let selection = editor.selections.newest_adjusted(cx);
|
||||
let Some((buffer, _, _)) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
|
||||
@@ -27,6 +27,7 @@ pub fn marked_display_snapshot(
|
||||
let font = Font {
|
||||
family: "Zed Plex Mono".into(),
|
||||
features: FontFeatures::default(),
|
||||
fallbacks: None,
|
||||
weight: FontWeight::default(),
|
||||
style: FontStyle::default(),
|
||||
};
|
||||
@@ -62,6 +63,7 @@ pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContex
|
||||
editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_text_with_selections(
|
||||
editor: &mut Editor,
|
||||
marked_text: &str,
|
||||
|
||||
@@ -327,7 +327,7 @@ impl EditorTestContext {
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<Tag>())
|
||||
.map(|h| h.1.clone())
|
||||
.unwrap_or_else(|| Arc::from([]))
|
||||
.unwrap_or_else(|| Arc::default())
|
||||
.into_iter()
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect()
|
||||
|
||||
@@ -21,7 +21,6 @@ assistant_slash_command.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
cap-std.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -363,6 +363,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = store.update(cx, |store, cx| store.reload(None, cx));
|
||||
|
||||
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
|
||||
@@ -159,29 +159,25 @@ impl WasmHost {
|
||||
}
|
||||
|
||||
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
|
||||
let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
|
||||
self.fs
|
||||
.create_dir(&extension_work_dir)
|
||||
.await
|
||||
.context("failed to create extension work dir")?;
|
||||
|
||||
let work_dir_preopen = Dir::open_ambient_dir(&extension_work_dir, ambient_authority())
|
||||
.context("failed to preopen extension work directory")?;
|
||||
let current_dir_preopen = work_dir_preopen
|
||||
.try_clone()
|
||||
.context("failed to preopen extension current directory")?;
|
||||
let extension_work_dir = extension_work_dir.to_string_lossy();
|
||||
|
||||
let perms = wasi::FilePerms::all();
|
||||
let file_perms = wasi::FilePerms::all();
|
||||
let dir_perms = wasi::DirPerms::all();
|
||||
|
||||
Ok(wasi::WasiCtxBuilder::new()
|
||||
.inherit_stdio()
|
||||
.preopened_dir(current_dir_preopen, dir_perms, perms, ".")
|
||||
.preopened_dir(work_dir_preopen, dir_perms, perms, &extension_work_dir)
|
||||
.env("PWD", &extension_work_dir)
|
||||
.preopened_dir(&extension_work_dir, ".", dir_perms, file_perms)?
|
||||
.preopened_dir(
|
||||
&extension_work_dir,
|
||||
&extension_work_dir.to_string_lossy(),
|
||||
dir_perms,
|
||||
file_perms,
|
||||
)?
|
||||
.env("PWD", &extension_work_dir.to_string_lossy())
|
||||
.env("RUST_BACKTRACE", "full")
|
||||
.build())
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn new_linker(
|
||||
f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
|
||||
) -> Linker<WasmState> {
|
||||
let mut linker = Linker::new(&wasm_engine());
|
||||
wasmtime_wasi::command::add_to_linker(&mut linker).unwrap();
|
||||
wasmtime_wasi::add_to_linker_async(&mut linker).unwrap();
|
||||
f(&mut linker, wasi_view).unwrap();
|
||||
linker
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 1);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
trappable_imports: true,
|
||||
path: "../extension_api/wit/since_v0.0.1",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
|
||||
@@ -11,6 +11,7 @@ pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
trappable_imports: true,
|
||||
path: "../extension_api/wit/since_v0.0.4",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
trappable_imports: true,
|
||||
path: "../extension_api/wit/since_v0.0.6",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
|
||||
@@ -26,6 +26,7 @@ pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
trappable_imports: true,
|
||||
path: "../extension_api/wit/since_v0.0.7",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
|
||||
@@ -134,6 +134,7 @@ impl ExtensionFilter {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum Feature {
|
||||
Git,
|
||||
OpenIn,
|
||||
Vim,
|
||||
LanguageBash,
|
||||
LanguageC,
|
||||
@@ -150,6 +151,19 @@ fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
|
||||
KEYWORDS_BY_FEATURE.get_or_init(|| {
|
||||
BTreeMap::from_iter([
|
||||
(Feature::Git, vec!["git"]),
|
||||
(
|
||||
Feature::OpenIn,
|
||||
vec![
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"codeberg",
|
||||
"sourcehut",
|
||||
"permalink",
|
||||
"link",
|
||||
"open in",
|
||||
],
|
||||
),
|
||||
(Feature::Vim, vec!["vim"]),
|
||||
(Feature::LanguageBash, vec!["sh", "bash"]),
|
||||
(Feature::LanguageC, vec!["c", "clang"]),
|
||||
@@ -802,6 +816,7 @@ impl ExtensionsPage {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
@@ -957,6 +972,11 @@ impl ExtensionsPage {
|
||||
"Zed comes with basic Git support. More Git features are coming in the future.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git"),
|
||||
Feature::OpenIn => FeatureUpsell::new(
|
||||
telemetry,
|
||||
"Zed supports linking to a source line on GitHub and others.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git#git-integrations"),
|
||||
Feature::Vim => FeatureUpsell::new(telemetry, "Vim support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/vim")
|
||||
.child(CheckboxWithLabel::new(
|
||||
|
||||
@@ -998,7 +998,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b0.5")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1006,7 +1006,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("c1.0")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1014,7 +1014,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a1.0")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1022,7 +1022,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a0.5")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1030,7 +1030,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b1.0")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
];
|
||||
@@ -1044,7 +1044,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a1.0")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1052,7 +1052,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b1.0")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1060,7 +1060,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("c1.0")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1068,7 +1068,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a0.5")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
@@ -1076,7 +1076,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b0.5")),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -821,6 +821,11 @@ impl FakeFs {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_next_mtime(&self, next_mtime: SystemTime) {
|
||||
let mut state = self.state.lock();
|
||||
state.next_mtime = next_mtime;
|
||||
}
|
||||
|
||||
pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
|
||||
self.write_file_internal(path, content).unwrap()
|
||||
}
|
||||
|
||||
@@ -404,7 +404,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_match_multibyte_path_entries() {
|
||||
let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"];
|
||||
let paths = vec![
|
||||
"aαbβ/cγdδ",
|
||||
"αβγδ/bcde",
|
||||
"c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f",
|
||||
"/d/🆒/h",
|
||||
];
|
||||
assert_eq!("1️⃣".len(), 7);
|
||||
assert_eq!(
|
||||
match_single_path_query("bcd", false, &paths),
|
||||
|
||||
@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
|
||||
worktree_id,
|
||||
positions: Vec::new(),
|
||||
path: Arc::from(candidate.path),
|
||||
path_prefix: Arc::from(""),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/google_ai.rs"
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ thiserror.workspace = true
|
||||
time.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
waker-fn = "1.1.0"
|
||||
waker-fn = "1.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
backtrace = "0.3"
|
||||
@@ -93,6 +93,7 @@ cbindgen = { version = "0.26.0", default-features = false }
|
||||
block = "0.1"
|
||||
cocoa.workspace = true
|
||||
core-foundation.workspace = true
|
||||
core-foundation-sys = "0.8"
|
||||
core-graphics = "0.23"
|
||||
core-text = "20.1"
|
||||
foreign-types = "0.5"
|
||||
@@ -150,7 +151,7 @@ x11-clipboard = "0.9.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
windows-core = "0.57"
|
||||
windows-core = "0.58"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
|
||||
50
crates/gpui/examples/gif_viewer.rs
Normal file
50
crates/gpui/examples/gif_viewer.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use gpui::{
|
||||
div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct GifViewer {
|
||||
gif_path: PathBuf,
|
||||
}
|
||||
|
||||
impl GifViewer {
|
||||
fn new(gif_path: PathBuf) -> Self {
|
||||
Self { gif_path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GifViewer {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().size_full().child(
|
||||
img(ImageSource::File(self.gif_path.clone().into()))
|
||||
.size_full()
|
||||
.object_fit(gpui::ObjectFit::Contain)
|
||||
.id("gif"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let cwd = std::env::current_dir().expect("Failed to get current working directory");
|
||||
let gif_path = cwd.join("crates/gpui/examples/image/black-cat-typing.gif");
|
||||
|
||||
if !gif_path.exists() {
|
||||
eprintln!("Image file not found at {:?}", gif_path);
|
||||
eprintln!("Make sure you're running this example from the root of the gpui crate");
|
||||
cx.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|_cx| GifViewer::new(gif_path)),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
BIN
crates/gpui/examples/image/black-cat-typing.gif
Normal file
BIN
crates/gpui/examples/image/black-cat-typing.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 MiB |
@@ -1,6 +1,7 @@
|
||||
use crate::{size, DevicePixels, Result, SharedString, Size};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use image::RgbaImage;
|
||||
use image::{Delay, Frame};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
@@ -34,43 +35,54 @@ pub struct ImageId(usize);
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub(crate) struct RenderImageParams {
|
||||
pub(crate) image_id: ImageId,
|
||||
pub(crate) frame_index: usize,
|
||||
}
|
||||
|
||||
/// A cached and processed image.
|
||||
pub struct ImageData {
|
||||
/// The ID associated with this image
|
||||
pub id: ImageId,
|
||||
data: RgbaImage,
|
||||
data: SmallVec<[Frame; 1]>,
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
/// Create a new image from the given data.
|
||||
pub fn new(data: RgbaImage) -> Self {
|
||||
pub fn new(data: impl Into<SmallVec<[Frame; 1]>>) -> Self {
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
Self {
|
||||
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
|
||||
data,
|
||||
data: data.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this image into a byte slice.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
pub fn as_bytes(&self, frame_index: usize) -> &[u8] {
|
||||
&self.data[frame_index].buffer()
|
||||
}
|
||||
|
||||
/// Get the size of this image, in pixels
|
||||
pub fn size(&self) -> Size<DevicePixels> {
|
||||
let (width, height) = self.data.dimensions();
|
||||
/// Get the size of this image, in pixels.
|
||||
pub fn size(&self, frame_index: usize) -> Size<DevicePixels> {
|
||||
let (width, height) = self.data[frame_index].buffer().dimensions();
|
||||
size(width.into(), height.into())
|
||||
}
|
||||
|
||||
/// Get the delay of this frame from the previous
|
||||
pub fn delay(&self, frame_index: usize) -> Delay {
|
||||
self.data[frame_index].delay()
|
||||
}
|
||||
|
||||
/// Get the number of frames for this image.
|
||||
pub fn frame_count(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ImageData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ImageData")
|
||||
.field("id", &self.id)
|
||||
.field("size", &self.data.dimensions())
|
||||
.field("size", &self.size(0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,14 +323,14 @@ impl Interactivity {
|
||||
pub fn on_boxed_action(
|
||||
&mut self,
|
||||
action: &dyn Action,
|
||||
listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
|
||||
listener: impl Fn(&dyn Action, &mut WindowContext) + 'static,
|
||||
) {
|
||||
let action = action.boxed_clone();
|
||||
self.action_listeners.push((
|
||||
(*action).type_id(),
|
||||
Box::new(move |_, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
(listener)(&action, cx)
|
||||
(listener)(&*action, cx)
|
||||
}
|
||||
}),
|
||||
));
|
||||
@@ -757,7 +757,7 @@ pub trait InteractiveElement: Sized {
|
||||
fn on_boxed_action(
|
||||
mut self,
|
||||
action: &dyn Action,
|
||||
listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
|
||||
listener: impl Fn(&dyn Action, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.interactivity().on_boxed_action(action, listener);
|
||||
self
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
|
||||
ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
|
||||
@@ -9,11 +5,20 @@ use crate::{
|
||||
WindowContext,
|
||||
};
|
||||
use futures::{AsyncReadExt, Future};
|
||||
use image::{ImageBuffer, ImageError};
|
||||
use http_client;
|
||||
use image::{
|
||||
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use media::core_video::CVImageBuffer;
|
||||
|
||||
use http_client;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -230,8 +235,14 @@ impl Img {
|
||||
}
|
||||
}
|
||||
|
||||
/// The image state between frames
|
||||
struct ImgState {
|
||||
frame_index: usize,
|
||||
last_frame_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Element for Img {
|
||||
type RequestLayoutState = ();
|
||||
type RequestLayoutState = usize;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -243,29 +254,65 @@ impl Element for Img {
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.data(cx) {
|
||||
let image_size = data.size();
|
||||
match (style.size.width, style.size.height) {
|
||||
(Length::Auto, Length::Auto) => {
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
cx.with_optional_element_state(global_id, |state, cx| {
|
||||
let mut state = state.map(|state| {
|
||||
state.unwrap_or(ImgState {
|
||||
frame_index: 0,
|
||||
last_frame_time: None,
|
||||
})
|
||||
});
|
||||
|
||||
let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
|
||||
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.data(cx) {
|
||||
if let Some(state) = &mut state {
|
||||
let frame_count = data.frame_count();
|
||||
if frame_count > 1 {
|
||||
let current_time = Instant::now();
|
||||
if let Some(last_frame_time) = state.last_frame_time {
|
||||
let elapsed = current_time - last_frame_time;
|
||||
let frame_duration =
|
||||
Duration::from(data.delay(state.frame_index));
|
||||
|
||||
if elapsed >= frame_duration {
|
||||
state.frame_index = (state.frame_index + 1) % frame_count;
|
||||
state.last_frame_time =
|
||||
Some(current_time - (elapsed - frame_duration));
|
||||
}
|
||||
} else {
|
||||
state.last_frame_time = Some(current_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(style, [])
|
||||
});
|
||||
(layout_id, ())
|
||||
let image_size = data.size(frame_index);
|
||||
match (style.size.width, style.size.height) {
|
||||
(Length::Auto, Length::Auto) => {
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if global_id.is_some() && data.frame_count() > 1 {
|
||||
cx.request_animation_frame();
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(style, [])
|
||||
});
|
||||
|
||||
((layout_id, frame_index), state)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
@@ -283,7 +330,7 @@ impl Element for Img {
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
frame_index: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
@@ -293,9 +340,15 @@ impl Element for Img {
|
||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
|
||||
if let Some(data) = source.data(cx) {
|
||||
let new_bounds = self.object_fit.get_bounds(bounds, data.size());
|
||||
cx.paint_image(new_bounds, corner_radii, data.clone(), self.grayscale)
|
||||
.log_err();
|
||||
let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
|
||||
cx.paint_image(
|
||||
new_bounds,
|
||||
corner_radii,
|
||||
data.clone(),
|
||||
*frame_index,
|
||||
self.grayscale,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
|
||||
match source {
|
||||
@@ -385,12 +438,34 @@ impl Asset for Image {
|
||||
};
|
||||
|
||||
let data = if let Ok(format) = image::guess_format(&bytes) {
|
||||
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
|
||||
let data = match format {
|
||||
ImageFormat::Gif => {
|
||||
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
|
||||
let mut frames = SmallVec::new();
|
||||
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
for frame in decoder.into_frames() {
|
||||
let mut frame = frame?;
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
frames.push(frame);
|
||||
}
|
||||
|
||||
frames
|
||||
}
|
||||
_ => {
|
||||
let mut data =
|
||||
image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
|
||||
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
|
||||
SmallVec::from_elem(Frame::new(data), 1)
|
||||
}
|
||||
};
|
||||
|
||||
ImageData::new(data)
|
||||
} else {
|
||||
@@ -400,7 +475,7 @@ impl Asset for Image {
|
||||
let buffer =
|
||||
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
|
||||
|
||||
ImageData::new(buffer)
|
||||
ImageData::new(SmallVec::from_elem(Frame::new(buffer), 1))
|
||||
};
|
||||
|
||||
Ok(Arc::new(data))
|
||||
|
||||
@@ -180,7 +180,7 @@ impl Transformation {
|
||||
}
|
||||
|
||||
fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
|
||||
//Note: if you read this as a sequence of matrix mulitplications, start from the bottom
|
||||
//Note: if you read this as a sequence of matrix multiplications, start from the bottom
|
||||
TransformationMatrix::unit()
|
||||
.translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
|
||||
.rotate(self.rotate)
|
||||
|
||||
@@ -940,6 +940,15 @@ where
|
||||
pub fn half_perimeter(&self) -> T {
|
||||
self.size.width.clone() + self.size.height.clone()
|
||||
}
|
||||
|
||||
/// centered_at creates a new bounds centered at the given point.
|
||||
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
|
||||
let origin = Point {
|
||||
x: center.x - size.width.half(),
|
||||
y: center.y - size.height.half(),
|
||||
};
|
||||
Self::new(origin, size)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
mod app_menu;
|
||||
mod keystroke;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod cosmic_text;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
@@ -51,8 +48,6 @@ use uuid::Uuid;
|
||||
pub use app_menu::*;
|
||||
pub use keystroke::*;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(crate) use cosmic_text::*;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) use linux::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -105,7 +100,6 @@ pub fn guess_compositor() -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
// todo("windows")
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
|
||||
Rc::new(WindowsPlatform::new())
|
||||
@@ -413,8 +407,6 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
|
||||
#[cfg(target_os = "windows")]
|
||||
fn destroy(&self);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
@@ -714,7 +706,6 @@ pub(crate) struct WindowParams {
|
||||
|
||||
pub display_id: Option<DisplayId>,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
mod text_system;
|
||||
|
||||
pub(crate) use text_system::*;
|
||||
@@ -237,14 +237,15 @@ pub struct Modifiers {
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
/// Returns true if any modifier key is pressed
|
||||
/// Returns whether any modifier key is pressed.
|
||||
pub fn modified(&self) -> bool {
|
||||
self.control || self.alt || self.shift || self.platform || self.function
|
||||
}
|
||||
|
||||
/// Whether the semantically 'secondary' modifier key is pressed
|
||||
/// On macos, this is the command key
|
||||
/// On windows and linux, this is the control key
|
||||
/// Whether the semantically 'secondary' modifier key is pressed.
|
||||
///
|
||||
/// On macOS, this is the command key.
|
||||
/// On Linux and Windows, this is the control key.
|
||||
pub fn secondary(&self) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -257,7 +258,7 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many modifier keys are pressed
|
||||
/// Returns how many modifier keys are pressed.
|
||||
pub fn number_of_modifiers(&self) -> u8 {
|
||||
self.control as u8
|
||||
+ self.alt as u8
|
||||
@@ -266,12 +267,12 @@ impl Modifiers {
|
||||
+ self.function as u8
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with no modifiers
|
||||
/// Returns [`Modifiers`] with no modifiers.
|
||||
pub fn none() -> Modifiers {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with just the command key
|
||||
/// Returns [`Modifiers`] with just the command key.
|
||||
pub fn command() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
@@ -279,7 +280,7 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper method for Modifiers with just the secondary key pressed
|
||||
/// A Returns [`Modifiers`] with just the secondary key pressed.
|
||||
pub fn secondary_key() -> Modifiers {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -298,7 +299,7 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with just the windows key
|
||||
/// Returns [`Modifiers`] with just the windows key.
|
||||
pub fn windows() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
@@ -306,7 +307,7 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with just the super key
|
||||
/// Returns [`Modifiers`] with just the super key.
|
||||
pub fn super_key() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
@@ -314,7 +315,7 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with just control
|
||||
/// Returns [`Modifiers`] with just control.
|
||||
pub fn control() -> Modifiers {
|
||||
Modifiers {
|
||||
control: true,
|
||||
@@ -322,7 +323,15 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with just shift
|
||||
/// Returns [`Modifiers`] with just control.
|
||||
pub fn alt() -> Modifiers {
|
||||
Modifiers {
|
||||
alt: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just shift.
|
||||
pub fn shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
@@ -330,7 +339,7 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with command + shift
|
||||
/// Returns [`Modifiers`] with command + shift.
|
||||
pub fn command_shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
@@ -339,7 +348,7 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with command + shift
|
||||
/// Returns [`Modifiers`] with command + shift.
|
||||
pub fn control_shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
@@ -348,7 +357,7 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if this Modifiers is a subset of another Modifiers
|
||||
/// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
|
||||
pub fn is_subset_of(&self, other: &Modifiers) -> bool {
|
||||
(other.control || !self.control)
|
||||
&& (other.alt || !self.alt)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user