Compare commits

..

100 Commits

Author SHA1 Message Date
Conrad Irwin
3f3f5c6ebd Merge branch 'main' into restore-max 2024-05-07 10:08:49 -06:00
Junkui Zhang
e9df71b124 fix linux BorrowMutError 2024-05-06 11:15:36 +08:00
Junkui Zhang
23997f3331 fixs 2024-05-05 23:41:55 +08:00
Junkui Zhang
ca6285bbe6 rename window_bounds() to bounds(), restore_status() to window_bounds() 2024-05-05 23:41:55 +08:00
Junkui Zhang
4d19d22b3f fix example 2024-05-05 23:41:55 +08:00
Junkui Zhang
f1c097568f update wayland 2024-05-05 23:41:55 +08:00
Junkui Zhang
2b00564518 revert macOS maximized impl 2024-05-05 23:41:54 +08:00
Junkui Zhang
33da303148 add comments 2024-05-05 23:41:54 +08:00
Junkui Zhang
4d656dba84 update editor_tests 2024-05-05 23:41:54 +08:00
Junkui Zhang
ca5dfaed8c update storybook 2024-05-05 23:41:54 +08:00
Junkui Zhang
74cb91ee7d fix 2024-05-05 23:41:54 +08:00
Junkui Zhang
22b1760486 update macOS 2024-05-05 23:41:54 +08:00
Junkui Zhang
3560bf7e51 update linux 2024-05-05 23:41:19 +08:00
Junkui Zhang
38f889ac97 update examples 2024-05-05 23:41:19 +08:00
Junkui Zhang
9a1bbb2dd3 update test_context 2024-05-05 23:41:19 +08:00
Junkui Zhang
7debce8b4b update x11/window.rs 2024-05-05 23:41:19 +08:00
Junkui Zhang
8d8490b79a update tests 2024-05-05 23:41:19 +08:00
Junkui Zhang
afe58e42ba remove unused import 2024-05-05 23:41:18 +08:00
Junkui Zhang
fadbf4690a rename SerializedWindowOpenStatus to SerializedWindowBounds 2024-05-05 23:41:18 +08:00
Junkui Zhang
eb9da1888b update zed.rs 2024-05-05 23:41:18 +08:00
Junkui Zhang
e7bc90ef91 update collab_ui 2024-05-05 23:41:18 +08:00
Junkui Zhang
a06ce20503 udpate collab_ui 2024-05-05 23:41:18 +08:00
Junkui Zhang
5b6899a226 update workspace 2024-05-05 23:41:18 +08:00
Junkui Zhang
b5ae7bc893 update SerializedWindowOpenStatus 2024-05-05 23:41:18 +08:00
Junkui Zhang
8b4273ee5e rename WindowOpenStatue to WindowBounds 2024-05-05 23:41:18 +08:00
Junkui Zhang
9eeb45d67b fix merge conficts 2024-05-05 23:41:18 +08:00
Junkui Zhang
04e8a44c23 remove macOS function 2024-05-05 23:41:18 +08:00
Junkui Zhang
cfbbaeab50 remove is_minimized 2024-05-05 23:41:18 +08:00
Junkui Zhang
1486fe0a59 remove unneeded is_minimized 2024-05-05 23:41:18 +08:00
Junkui Zhang
af26d6bbcf remove comments 2024-05-05 23:41:18 +08:00
Junkui Zhang
882ced6949 update windows::window 2024-05-05 23:41:18 +08:00
Junkui Zhang
f39e977363 remove unneeded restore bounds 2024-05-05 23:41:18 +08:00
Junkui Zhang
107dd9081d fix error 2024-05-05 23:41:18 +08:00
Junkui Zhang
7502b46c1f revert some changes 2024-05-05 23:41:18 +08:00
Junkui Zhang
ee9579a84f update wayland 2024-05-05 23:41:18 +08:00
Junkui Zhang
b9818c6984 update test 2024-05-05 23:41:17 +08:00
Junkui Zhang
7f7755af68 update 2024-05-05 23:41:17 +08:00
Junkui Zhang
5bfb26510f update rename 2024-05-05 23:41:17 +08:00
Junkui Zhang
a32346db25 fix 2024-05-05 23:41:17 +08:00
Junkui Zhang
8d0d02b887 update wayland 2024-05-05 23:41:17 +08:00
Junkui Zhang
06f3f52159 rename FullScreen to Fullscreen 2024-05-05 23:41:17 +08:00
Junkui Zhang
2a3048aca7 update Workspace 2024-05-05 23:41:17 +08:00
Junkui Zhang
f2cf9fefe7 remove unused 2024-05-05 23:41:17 +08:00
Junkui Zhang
dab7b8220b update WaylandWindow 2024-05-05 23:41:17 +08:00
Junkui Zhang
1e9fd8364d udpate MacWindow 2024-05-05 23:41:17 +08:00
Junkui Zhang
ae1428679a update X11Window 2024-05-05 23:41:17 +08:00
Junkui Zhang
80628911b2 update MacWindow 2024-05-05 23:41:17 +08:00
Junkui Zhang
040babebf9 update TestWindow 2024-05-05 23:41:17 +08:00
Junkui Zhang
9032a8b111 update linux 2024-05-05 23:41:17 +08:00
Junkui Zhang
b9228f1dc1 update Windows 2024-05-05 23:41:17 +08:00
Junkui Zhang
d4b7e88aeb refactor window open action 2024-05-05 23:41:17 +08:00
Junkui Zhang
7275d61f82 revert WindowParams 2024-05-05 23:41:17 +08:00
Junkui Zhang
451cd47a7e fix x11 2024-05-05 23:41:00 +08:00
Junkui Zhang
68d1ccfeb1 Linux init 2024-05-05 23:41:00 +08:00
Junkui Zhang
0c17f691c0 remove debug print 2024-05-05 23:41:00 +08:00
Junkui Zhang
d130d3e02e fix fullscreen 2024-05-05 23:41:00 +08:00
Junkui Zhang
d541a75e3c fix err 2024-05-05 23:41:00 +08:00
Junkui Zhang
1ccad5b078 fix db error 2024-05-05 23:41:00 +08:00
Junkui Zhang
696d496367 move maximize 2024-05-05 23:41:00 +08:00
Junkui Zhang
20d9a3e8db macOS init 2024-05-05 23:41:00 +08:00
Junkui Zhang
691a2e983d update macOS 2024-05-05 23:41:00 +08:00
Junkui Zhang
2c5a7ddd00 macOS init 2024-05-05 23:40:43 +08:00
Junkui Zhang
2b2d5ea813 fmt 2024-05-05 23:40:43 +08:00
Junkui Zhang
8b8ef5fbe4 fix story book 2024-05-05 23:40:43 +08:00
Junkui Zhang
03c60f943e fix workspace 2024-05-05 23:40:42 +08:00
Junkui Zhang
48f875f9d8 try fix test 2024-05-05 23:40:42 +08:00
Junkui Zhang
b0d43e51bb try fix test 2024-05-05 23:40:42 +08:00
Junkui Zhang
92481a93aa use bounds 2024-05-05 23:40:42 +08:00
Junkui Zhang
300121f5cc update tests 2024-05-05 23:40:42 +08:00
Junkui Zhang
056adae16c remove debug print 2024-05-05 23:40:42 +08:00
Junkui Zhang
24e07874b3 catch up main branch changes 2024-05-05 23:40:42 +08:00
Junkui Zhang
fbd87c96d0 remember fullscreen restore size 2024-05-05 23:40:42 +08:00
Junkui Zhang
dabce12916 add get_bounds 2024-05-05 23:40:42 +08:00
Junkui Zhang
84369f3e4b remove unused variables 2024-05-05 23:40:42 +08:00
Junkui Zhang
d54ba0e579 use GetWindowPlacement to retrieve information needed 2024-05-05 23:40:42 +08:00
Junkui Zhang
7fca2173ce try another approch 2024-05-05 23:40:42 +08:00
Junkui Zhang
8034b76867 windows impl 2024-05-05 23:40:42 +08:00
Junkui Zhang
de38629efb fix 2024-05-05 23:40:42 +08:00
Junkui Zhang
7a4f6677cc update windows impl 2024-05-05 23:40:42 +08:00
Junkui Zhang
0cd31c8da3 report restore size 2024-05-05 23:40:42 +08:00
Junkui Zhang
2410470d72 change function 2024-05-05 23:40:42 +08:00
Junkui Zhang
ab6ff3ffcc update windows window creatation 2024-05-05 23:40:42 +08:00
Junkui Zhang
f261cf513a udpate serialization and deserial 2024-05-05 23:40:42 +08:00
Junkui Zhang
b1c01ef45c add restore size 2024-05-05 23:40:42 +08:00
Junkui Zhang
8616381ed9 remove fullscreen 2024-05-05 23:40:42 +08:00
Junkui Zhang
66d702922e remove unused 2024-05-05 23:40:42 +08:00
Junkui Zhang
0386f76e55 fix bug 2024-05-05 23:40:42 +08:00
Junkui Zhang
ae8f9ad22f fix zed.rs 2024-05-05 23:40:41 +08:00
Junkui Zhang
608469eca8 fix typo 2024-05-05 23:40:41 +08:00
Junkui Zhang
9fe0184cad try fix test 2024-05-05 23:40:41 +08:00
Junkui Zhang
4669b14c57 fix workspace_for_roots 2024-05-05 23:40:41 +08:00
Junkui Zhang
40fe820aae fix typo 2024-05-05 23:40:41 +08:00
Junkui Zhang
ea086316ed fix join project 2024-05-05 23:40:41 +08:00
Junkui Zhang
c62970a6cc update SerializedWorkspace 2024-05-05 23:40:41 +08:00
Junkui Zhang
218a7e2b69 impl serialization 2024-05-05 23:40:41 +08:00
Junkui Zhang
a8b9393477 fix gpui::window 2024-05-05 23:40:41 +08:00
Junkui Zhang
6c4d5bcbd5 fix Windows impl 2024-05-05 23:40:41 +08:00
Junkui Zhang
e72b94bbbf fix collab 2024-05-05 23:40:41 +08:00
Junkui Zhang
49e085037e change WindowParams and WindowOptions 2024-05-05 23:40:41 +08:00
Junkui Zhang
7c0ea926f0 init 2024-05-05 23:40:09 +08:00
813 changed files with 31379 additions and 65223 deletions

View File

@@ -4,11 +4,3 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
[alias]
xtask = "run --package xtask --"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View File

@@ -1,15 +0,0 @@
We have two cloudflare workers that let us serve some assets of this repo
from Cloudflare.
* `open-source-website-assets` is used for `install.sh`
* `docs-proxy` is used for `https://zed.dev/docs`
On push to `main`, both of these (and the files they depend on) are uploaded to Cloudflare.
### Deployment
These functions are deployed on push to main by the deploy_cloudflare.yml workflow. Worker Rules in Cloudflare intercept requests to zed.dev and proxy them to the appropriate workers.
### Testing
You can use [wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update) to test these workers locally, or to deploy custom versions.

View File

@@ -1,14 +0,0 @@
export default {
async fetch(request, _env, _ctx) {
const url = new URL(request.url);
url.hostname = "docs-anw.pages.dev";
let res = await fetch(url, request);
if (res.status === 404) {
res = await fetch("https://zed.dev/404");
}
return res;
},
};

View File

@@ -1,8 +0,0 @@
name = "docs-proxy"
main = "src/worker.js"
compatibility_date = "2024-05-03"
workers_dev = true
[[routes]]
pattern = "zed.dev/docs*"
zone_name = "zed.dev"

View File

@@ -1,19 +0,0 @@
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const object = await env.OPEN_SOURCE_WEBSITE_ASSETS_BUCKET.get(key);
if (!object) {
return await fetch("https://zed.dev/404");
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
return new Response(object.body, {
headers,
});
},
};

View File

@@ -1,8 +0,0 @@
name = "open-source-website-assets"
main = "src/worker.js"
compatibility_date = "2024-05-15"
workers_dev = true
[[r2_buckets]]
binding = 'OPEN_SOURCE_WEBSITE_ASSETS_BUCKET'
bucket_name = 'zed-open-source-website-assets'

View File

@@ -6,8 +6,6 @@ Release Notes:
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
### Or...
Release Notes:
**or**
- N/A

View File

@@ -23,7 +23,6 @@ env:
jobs:
style:
timeout-minutes: 60
name: Check formatting and spelling
runs-on:
- self-hosted
@@ -33,6 +32,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Remove untracked files
@@ -74,11 +74,10 @@ jobs:
version: v1.29.0
- uses: bufbuild/buf-breaking-action@v1
with:
input: "crates/proto/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
input: "crates/rpc/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
runs-on:
- self-hosted
@@ -88,6 +87,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: cargo clippy
run: cargo xtask clippy
@@ -101,40 +101,42 @@ jobs:
- name: Build other binaries and features
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
# todo(linux): Actually run the tests
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
runs-on:
- self-hosted
- deploy
runs-on: ubuntu-latest
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: cargo clippy
run: cargo xtask clippy
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build Zed
run: cargo build -p zed
# todo(windows): Actually run the tests
windows_tests:
timeout-minutes: 60
name: (Windows) Run Clippy and tests
runs-on: hosted-windows-1
runs-on: windows-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
@@ -148,7 +150,6 @@ jobs:
run: cargo build -p zed
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
runs-on:
- self-hosted
@@ -178,6 +179,7 @@ jobs:
# 25 was chosen arbitrarily.
fetch-depth: 25
clean: false
submodules: "recursive"
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
@@ -259,26 +261,27 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux:
timeout-minutes: 60
name: Create a Linux bundle
runs-on:
- self-hosted
- deploy
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
@@ -307,6 +310,9 @@ jobs:
exit 1
fi
- name: Generate license file
run: script/generate-licenses
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux
@@ -327,86 +333,3 @@ jobs:
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux-aarch64:
timeout-minutes: 60
name: Create arm64 Linux bundle
runs-on:
- hosted-linux-arm-1
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
- name: "Setup jq"
uses: dcarbone/install-jq-action@v2
- name: Set up Clang
run: |
sudo apt-get update
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@v1
with:
mold-version: 2.32.0
- name: rustup
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
set -eu
version=$(script/get-crate-version zed)
channel=$(cat crates/zed/RELEASE_CHANNEL)
echo "Publishing version: ${version} on release channel ${channel}"
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
expected_tag_name=""
case ${channel} in
stable)
expected_tag_name="v${version}";;
preview)
expected_tag_name="v${version}-pre";;
nightly)
expected_tag_name="v${version}-nightly";;
*)
echo "can't publish a release on channel ${channel}"
exit 1;;
esac
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
exit 1
fi
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@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
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: target/release/zed-linux-aarch64.tar.gz
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -18,7 +18,7 @@ jobs:
- uses: pnpm/action-setup@v3
with:
version: 9
version: 8
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -1,56 +0,0 @@
name: Deploy Docs
on:
push:
branches:
- main
jobs:
deploy-docs:
name: Deploy Docs
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: "0.4.37"
- name: Build book
run: |
set -euo pipefail
mkdir -p target/deploy
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy Docs
uses: cloudflare/wrangler-action@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
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
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
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy .cloudflare/docs-proxy/src/worker.js

View File

@@ -21,6 +21,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Run style checks
@@ -40,6 +41,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Install cargo nextest
@@ -74,6 +76,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Build docker image
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA

35
.github/workflows/deploy_docs.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Deploy Docs
on:
push:
branches:
- main
jobs:
deploy-docs:
name: Deploy Docs
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: "0.4.37"
- name: Build book
run: |
set -euo pipefail
mkdir -p target/deploy
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs

View File

@@ -19,6 +19,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2

View File

@@ -31,6 +31,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Run randomized tests
run: script/randomized-test-ci

View File

@@ -15,7 +15,6 @@ env:
jobs:
style:
timeout-minutes: 60
name: Check formatting and Clippy lints
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -26,6 +25,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Run style checks
@@ -34,7 +34,6 @@ jobs:
- name: Run clippy
run: cargo xtask clippy
tests:
timeout-minutes: 60
name: Run tests
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -46,12 +45,12 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Run tests
uses: ./.github/actions/run_tests
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -76,6 +75,7 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Set release channel to nightly
run: |
@@ -94,12 +94,9 @@ jobs:
run: script/upload-nightly macos
bundle-deb:
timeout-minutes: 60
name: Create a Linux *.tar.gz bundle
if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- deploy
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
@@ -110,9 +107,16 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: Set release channel to nightly
run: |

4
.gitignore vendored
View File

@@ -7,8 +7,6 @@
/script/node_modules
/crates/theme/schemas/theme.json
/crates/collab/seed.json
/crates/zed/resources/flatpak/flatpak-cargo-sources.json
/dev.zed.Zed*.json
/assets/*licenses.md
**/venv
.build
@@ -26,5 +24,3 @@ DerivedData/
.venv
.blob_store
.vscode
.wrangler
.flatpak-builder

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "crates/live_kit_server/protocol"]
path = crates/live_kit_server/protocol
url = https://github.com/livekit/protocol

View File

@@ -15,12 +15,8 @@ Christian Bergschneider <christian.bergschneider@gmx.de>
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
Ivan Žužak <izuzak@gmail.com>
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
Joseph T. Lyons <JosephTLyons@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
Julia <floc@unpromptedtirade.com>
@@ -33,9 +29,6 @@ Kirill Bulatov <kirill@zed.dev>
Kirill Bulatov <kirill@zed.dev> <mail4score@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com> <kyle@zed.dev>
LoganDark <contact@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marshall Bowers <elliott.codes@gmail.com>
Marshall Bowers <elliott.codes@gmail.com> <marshall@zed.dev>
Max Brunsfeld <maxbrunsfeld@gmail.com>
@@ -48,8 +41,6 @@ Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
Nathan Sobo <nathan@zed.dev>
Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
Petros Amoiridis <petros@hey.com>
Petros Amoiridis <petros@hey.com> <petros@zed.dev>
Piotr Osiewicz <piotr@zed.dev>
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
Robert Clover <git@clo4.net>

View File

@@ -3,5 +3,10 @@
"label": "clippy",
"command": "cargo",
"args": ["xtask", "clippy"]
},
{
"label": "assistant2",
"command": "cargo",
"args": ["run", "-p", "assistant2", "--example", "assistant_example"]
}
]

1158
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,8 @@ members = [
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant_slash_command",
"crates/assistant_tooling",
"crates/assistant2",
"crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
@@ -41,8 +41,6 @@ members = [
"crates/gpui",
"crates/gpui_macros",
"crates/headless",
"crates/html_to_markdown",
"crates/http",
"crates/image_viewer",
"crates/inline_completion_button",
"crates/install_cli",
@@ -54,23 +52,19 @@ members = [
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/markdown",
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/multi_buffer",
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
"crates/open_ai",
"crates/outline",
"crates/outline_panel",
"crates/picker",
"crates/prettier",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/proto",
"crates/quick_action_bar",
"crates/recent_projects",
"crates/refineable",
@@ -80,7 +74,6 @@ members = [
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rustdoc",
"crates/task",
"crates/tasks_ui",
"crates/search",
@@ -133,7 +126,6 @@ members = [
"extensions/php",
"extensions/prisma",
"extensions/purescript",
"extensions/ruby",
"extensions/svelte",
"extensions/terraform",
"extensions/toml",
@@ -152,9 +144,8 @@ ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant2 = { path = "crates/assistant2" }
assistant_tooling = { path = "crates/assistant_tooling" }
async-watch = "0.3.1"
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
base64 = "0.13"
@@ -167,10 +158,10 @@ clock = { path = "crates/clock" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
color = { path = "crates/color" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
copilot = { path = "crates/copilot" }
dashmap = "5.5.3"
db = { path = "crates/db" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
@@ -190,8 +181,6 @@ google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui" }
gpui_macros = { path = "crates/gpui_macros" }
headless = { path = "crates/headless" }
html_to_markdown = { path = "crates/html_to_markdown" }
http = { path = "crates/http" }
install_cli = { path = "crates/install_cli" }
image_viewer = { path = "crates/image_viewer" }
inline_completion_button = { path = "crates/inline_completion_button" }
@@ -203,23 +192,19 @@ languages = { path = "crates/languages" }
live_kit_client = { path = "crates/live_kit_client" }
live_kit_server = { path = "crates/live_kit_server" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
multi_buffer = { path = "crates/multi_buffer" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
open_ai = { path = "crates/open_ai" }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
prettier = { path = "crates/prettier" }
project = { path = "crates/project" }
proto = { path = "crates/proto" }
worktree = { path = "crates/worktree" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
@@ -230,7 +215,6 @@ dev_server_projects = { path = "crates/dev_server_projects" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rustdoc = { path = "crates/rustdoc" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
search = { path = "crates/search" }
@@ -241,7 +225,7 @@ snippet = { path = "crates/snippet" }
sqlez = { path = "crates/sqlez" }
sqlez_macros = { path = "crates/sqlez_macros" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
supermaven_api = { path = "crates/supermaven_api"}
story = { path = "crates/story" }
storybook = { path = "crates/storybook" }
sum_tree = { path = "crates/sum_tree" }
@@ -266,59 +250,50 @@ zed_actions = { path = "crates/zed_actions" }
anyhow = "1.0.57"
any_vec = "0.13"
ashpd = "0.8.0"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-fs = "1.6"
async-recursion = "1.0.0"
async-tar = "0.4.2"
async-trait = "0.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
blade-util = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = { version = "0.11.6" }
cocoa = "0.25"
ctor = "0.2.6"
signal-hook = "0.3.17"
ctrlc = "3.4.4"
core-foundation = { version = "0.9.3" }
core-foundation-sys = "0.8.6"
derive_more = "0.99.17"
emojis = "0.6.1"
env_logger = "0.9"
exec = "0.3.1"
fork = "0.1.23"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.18", default-features = false }
globset = "0.4"
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = [
"read-txn-no-tls",
] }
hex = "0.4.3"
html5ever = "0.27.0"
ignore = "0.4.22"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "1"
# We explicitly disable http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding" ] }
isahc = { version = "1.7.2", default-features = false, features = [
"static-curl",
"text-decoding",
] }
itertools = "0.11.0"
lazy_static = "1.4.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nix = "0.28"
num-format = "0.4.4"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
profiling = "1"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
@@ -329,9 +304,8 @@ pulldown-cmark = { version = "0.10.0", default-features = false }
rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
repair_json = "0.1.0"
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rust-embed = { version = "8.0", features = ["include-exclude"] }
schemars = "0.8"
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -344,8 +318,6 @@ serde_json_lenient = { version = "0.1", features = [
serde_repr = "0.1"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
similar = "1.3"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -353,7 +325,7 @@ subtle = "2.5.0"
sysinfo = "0.30.7"
tempfile = "3.9.0"
thiserror = "1.0.29"
tiktoken-rs = "0.5.9"
tiktoken-rs = "0.5.7"
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -365,33 +337,33 @@ 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-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
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-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
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-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
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-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
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-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
unindent = "0.1.7"
unicase = "2.6"
unicode-segmentation = "1.10"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
uuid = { version = "1.1.2", features = ["v4", "v5"] }
wasmparser = "0.201"
wasm-encoder = "0.201"
wasmtime = { version = "19.0.0", default-features = false, features = [
@@ -407,23 +379,20 @@ wit-component = "0.201"
sys-locale = "0.3.1"
[workspace.dependencies.windows]
version = "0.57"
version = "0.53.0"
features = [
"implement",
"Foundation_Numerics",
"System",
"System_Threading",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dwm",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Imaging_D2D",
"Win32_Media",
"Win32_Security",
"Win32_Security_Credentials",
"Win32_Storage_FileSystem",
@@ -437,7 +406,6 @@ features = [
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Time",
"Win32_System_WinRT",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
"Win32_UI_Input_Ime",

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.79-bookworm as builder
FROM rust:1.78-bookworm as builder
WORKDIR app
COPY . .

View File

@@ -1 +0,0 @@
<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-arrow-down-from-line"><path d="M19 3H5"/><path d="M12 21V7"/><path d="m6 15 6 6 6-6"/></svg>

Before

Width:  |  Height:  |  Size: 295 B

View File

@@ -1 +0,0 @@
<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-arrow-up-from-line"><path d="m18 9-6-6-6 6"/><path d="M12 3v14"/><path d="M5 21h14"/></svg>

Before

Width:  |  Height:  |  Size: 294 B

View File

@@ -1,6 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 8H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 10.9502H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 683 B

View File

@@ -1 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.15 7.49998C13.15 4.66458 10.9402 1.84998 7.50002 1.84998C4.7217 1.84998 3.34851 3.90636 2.76336 4.99997H4.5C4.77614 4.99997 5 5.22383 5 5.49997C5 5.77611 4.77614 5.99997 4.5 5.99997H1.5C1.22386 5.99997 1 5.77611 1 5.49997V2.49997C1 2.22383 1.22386 1.99997 1.5 1.99997C1.77614 1.99997 2 2.22383 2 2.49997V4.31318C2.70453 3.07126 4.33406 0.849976 7.50002 0.849976C11.5628 0.849976 14.15 4.18537 14.15 7.49998C14.15 10.8146 11.5628 14.15 7.50002 14.15C5.55618 14.15 3.93778 13.3808 2.78548 12.2084C2.16852 11.5806 1.68668 10.839 1.35816 10.0407C1.25306 9.78536 1.37488 9.49315 1.63024 9.38806C1.8856 9.28296 2.17781 9.40478 2.2829 9.66014C2.56374 10.3425 2.97495 10.9745 3.4987 11.5074C4.47052 12.4963 5.83496 13.15 7.50002 13.15C10.9402 13.15 13.15 10.3354 13.15 7.49998ZM7 10V5.00001H8V10H7Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 974 B

View File

@@ -1,15 +1,15 @@
{
"stems": {
"Dockerfile": "docker",
"Podfile": "ruby",
"Procfile": "heroku"
"Procfile": "heroku",
"Dockerfile": "docker"
},
"suffixes": {
"astro": "astro",
"Emakefile": "erlang",
"aac": "audio",
"accdb": "storage",
"app.src": "erlang",
"astro": "astro",
"avi": "video",
"avif": "image",
"bak": "backup",
@@ -22,12 +22,12 @@
"c": "c",
"cc": "cpp",
"cjs": "javascript",
"coffee": "coffeescript",
"conf": "settings",
"cpp": "cpp",
"css": "css",
"csv": "storage",
"cts": "typescript",
"coffee": "coffeescript",
"dart": "dart",
"dat": "storage",
"db": "storage",
@@ -57,16 +57,14 @@
"gitkeep": "vcs",
"gitmodules": "vcs",
"go": "go",
"gql": "graphql",
"graphql": "graphql",
"graphqls": "graphql",
"h": "c",
"hpp": "cpp",
"handlebars": "code",
"hbs": "template",
"heex": "elixir",
"heic": "image",
"heif": "image",
"hpp": "cpp",
"heic": "image",
"hrl": "erlang",
"hs": "haskell",
"htm": "template",
@@ -74,7 +72,6 @@
"ib": "storage",
"ico": "image",
"ini": "settings",
"inl": "cpp",
"j2k": "image",
"java": "java",
"jfif": "image",
@@ -82,9 +79,9 @@
"jpeg": "image",
"jpg": "image",
"js": "javascript",
"jsx": "react",
"json": "storage",
"jsonc": "storage",
"jsx": "react",
"jxl": "image",
"kt": "kotlin",
"ldf": "storage",
@@ -99,9 +96,9 @@
"mdf": "storage",
"mdx": "document",
"metadata": "code",
"mkv": "video",
"mjs": "javascript",
"mka": "audio",
"mkv": "video",
"ml": "ocaml",
"mli": "ocaml",
"mov": "video",
@@ -110,8 +107,8 @@
"mts": "typescript",
"myd": "storage",
"myi": "storage",
"nim": "nim",
"nu": "terminal",
"nim": "nim",
"odp": "document",
"ods": "document",
"odt": "document",
@@ -133,33 +130,33 @@
"psd": "image",
"py": "python",
"qoi": "image",
"r": "r",
"rb": "ruby",
"rebar.config": "erlang",
"rkt": "code",
"rs": "rust",
"r": "r",
"rtf": "document",
"sav": "storage",
"sc": "scala",
"scala": "scala",
"scm": "code",
"sdf": "storage",
"sh": "terminal",
"sql": "storage",
"sqlite": "storage",
"svelte": "template",
"svg": "image",
"sc": "scala",
"scala": "scala",
"sql": "storage",
"swift": "swift",
"tcl": "tcl",
"tf": "terraform",
"tfvars": "terraform",
"tiff": "image",
"toml": "toml",
"ts": "typescript",
"tsv": "storage",
"tsx": "react",
"ttf": "font",
"tsx": "react",
"txt": "document",
"tcl": "tcl",
"vue": "vue",
"wav": "audio",
"webm": "video",
@@ -191,30 +188,27 @@
"audio": {
"icon": "icons/file_icons/audio.svg"
},
"bun": {
"icon": "icons/file_icons/bun.svg"
},
"c": {
"icon": "icons/file_icons/c.svg"
},
"code": {
"icon": "icons/file_icons/code.svg"
},
"coffeescript": {
"icon": "icons/file_icons/coffeescript.svg"
},
"collapsed_chevron": {
"icon": "icons/file_icons/chevron_right.svg"
},
"collapsed_folder": {
"icon": "icons/file_icons/folder.svg"
},
"c": {
"icon": "icons/file_icons/c.svg"
},
"cpp": {
"icon": "icons/file_icons/cpp.svg"
},
"css": {
"icon": "icons/file_icons/css.svg"
},
"coffeescript": {
"icon": "icons/file_icons/coffeescript.svg"
},
"dart": {
"icon": "icons/file_icons/dart.svg"
},
@@ -251,18 +245,18 @@
"fsharp": {
"icon": "icons/file_icons/fsharp.svg"
},
"go": {
"icon": "icons/file_icons/go.svg"
},
"graphql": {
"icon": "icons/file_icons/graphql.svg"
},
"haskell": {
"icon": "icons/file_icons/haskell.svg"
},
"heroku": {
"icon": "icons/file_icons/heroku.svg"
},
"go": {
"icon": "icons/file_icons/go.svg"
},
"graphql": {
"icon": "icons/file_icons/graphql.svg"
},
"image": {
"icon": "icons/file_icons/image.svg"
},
@@ -278,18 +272,21 @@
"lock": {
"icon": "icons/file_icons/lock.svg"
},
"bun": {
"icon": "icons/file_icons/bun.svg"
},
"log": {
"icon": "icons/file_icons/info.svg"
},
"lua": {
"icon": "icons/file_icons/lua.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},
"ocaml": {
"icon": "icons/file_icons/ocaml.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},
"phoenix": {
"icon": "icons/file_icons/phoenix.svg"
},
@@ -317,36 +314,36 @@
"rust": {
"icon": "icons/file_icons/rust.svg"
},
"scala": {
"icon": "icons/file_icons/scala.svg"
},
"settings": {
"icon": "icons/file_icons/settings.svg"
},
"storage": {
"icon": "icons/file_icons/database.svg"
},
"scala": {
"icon": "icons/file_icons/scala.svg"
},
"swift": {
"icon": "icons/file_icons/swift.svg"
},
"tcl": {
"icon": "icons/file_icons/tcl.svg"
},
"template": {
"icon": "icons/file_icons/html.svg"
},
"terminal": {
"icon": "icons/file_icons/terminal.svg"
},
"terraform": {
"icon": "icons/file_icons/terraform.svg"
},
"terminal": {
"icon": "icons/file_icons/terminal.svg"
},
"toml": {
"icon": "icons/file_icons/toml.svg"
},
"typescript": {
"icon": "icons/file_icons/typescript.svg"
},
"tcl": {
"icon": "icons/file_icons/tcl.svg"
},
"vcs": {
"icon": "icons/file_icons/git.svg"
},

View File

@@ -1,5 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 6C1.5 6.89002 1.76392 7.76004 2.25839 8.50007C2.75285 9.24009 3.45566 9.81686 4.27792 10.1575C5.10019 10.4981 6.00499 10.5872 6.87791 10.4135C7.75082 10.2399 8.55264 9.81132 9.18198 9.18198C9.81132 8.55264 10.2399 7.75082 10.4135 6.87791C10.5872 6.00499 10.4981 5.10019 10.1575 4.27792C9.81686 3.45566 9.24009 2.75285 8.50007 2.25839C7.76004 1.76392 6.89002 1.5 6 1.5C4.74198 1.50473 3.53448 1.99561 2.63 2.87L1.5 4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.5 1.5V4H4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 3.5V6L8 7" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 778 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 15C13.433 15 15 13.433 15 11.5C15 9.567 13.433 8 11.5 8C9.567 8 8 9.567 8 11.5C8 13.433 9.567 15 11.5 15Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 240 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4662 14.9152C13.5801 15.0291 13.7648 15.0291 13.8787 14.9152L14.9145 13.8793C15.0284 13.7654 15.0284 13.5807 14.9145 13.4667L12.9483 11.5004L14.9145 9.53392C15.0285 9.42004 15.0285 9.23533 14.9145 9.12137L13.8787 8.08547C13.7648 7.97154 13.5801 7.97154 13.4662 8.08547L11.5 10.0519L9.53376 8.08545C9.41988 7.97152 9.23517 7.97152 9.12124 8.08545L8.08543 9.12136C7.97152 9.23533 7.97152 9.42004 8.08543 9.53392L10.0517 11.5004L8.08545 13.4667C7.97155 13.5807 7.97155 13.7654 8.08545 13.8793L9.12126 14.9152C9.23517 15.0292 9.41988 15.0292 9.53376 14.9152L11.5 12.9489L13.4662 14.9152Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 756 B

View File

@@ -1 +0,0 @@
<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-library"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>

Before

Width:  |  Height:  |  Size: 298 B

View File

@@ -1 +0,0 @@
<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-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>

Before

Width:  |  Height:  |  Size: 349 B

View File

@@ -1,7 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12C3 9.61305 3.94821 7.32387 5.63604 5.63604C7.32387 3.94821 9.61305 3 12 3C14.516 3.00947 16.931 3.99122 18.74 5.74L21 8" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 3V8H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C21 14.3869 20.0518 16.6761 18.364 18.364C16.6761 20.0518 14.3869 21 12 21C9.48395 20.9905 7.06897 20.0088 5.26 18.26L3 16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 16H3V21" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 9.37052C10 8.98462 10.4186 8.74419 10.7519 8.93863L15.2596 11.5681C15.5904 11.761 15.5904 12.2389 15.2596 12.4319L10.7519 15.0614C10.4186 15.2558 10 15.0154 10 14.6295V9.37052Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 949 B

View File

@@ -1 +0,0 @@
<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-folder-search"><circle cx="17" cy="17" r="3"/><path d="M10.7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4.1"/><path d="m21 21-1.5-1.5"/></svg>

Before

Width:  |  Height:  |  Size: 400 B

View File

@@ -1 +0,0 @@
<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-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>

Before

Width:  |  Height:  |  Size: 303 B

View File

@@ -1 +0,0 @@
<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-save"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>

Before

Width:  |  Height:  |  Size: 412 B

View File

@@ -1 +0,0 @@
<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-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>

Before

Width:  |  Height:  |  Size: 299 B

View File

@@ -1 +0,0 @@
<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-sparkle"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>

Before

Width:  |  Height:  |  Size: 481 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.937 15.5C9.84772 15.1539 9.66734 14.8381 9.41462 14.5854C9.1619 14.3327 8.84607 14.1523 8.5 14.063L2.365 12.481C2.26033 12.4513 2.16821 12.3883 2.10261 12.3014C2.03702 12.2146 2.00153 12.1088 2.00153 12C2.00153 11.8912 2.03702 11.7854 2.10261 11.6986C2.16821 11.6118 2.26033 11.5487 2.365 11.519L8.5 9.93601C8.84595 9.84681 9.16169 9.66658 9.4144 9.41404C9.66711 9.16151 9.84757 8.84589 9.937 8.50001L11.519 2.36501C11.5484 2.25992 11.6114 2.16735 11.6983 2.1014C11.7853 2.03545 11.8914 1.99976 12.0005 1.99976C12.1096 1.99976 12.2157 2.03545 12.3027 2.1014C12.3896 2.16735 12.4526 2.25992 12.482 2.36501L14.063 8.50001C14.1523 8.84608 14.3327 9.1619 14.5854 9.41462C14.8381 9.66734 15.1539 9.84773 15.5 9.93701L21.635 11.518C21.7405 11.5471 21.8335 11.61 21.8998 11.6971C21.9661 11.7841 22.0021 11.8906 22.0021 12C22.0021 12.1094 21.9661 12.2159 21.8998 12.3029C21.8335 12.39 21.7405 12.4529 21.635 12.482L15.5 14.063C15.1539 14.1523 14.8381 14.3327 14.5854 14.5854C14.3327 14.8381 14.1523 15.1539 14.063 15.5L12.481 21.635C12.4516 21.7401 12.3886 21.8327 12.3017 21.8986C12.2147 21.9646 12.1086 22.0003 11.9995 22.0003C11.8904 22.0003 11.7843 21.9646 11.6973 21.8986C11.6104 21.8327 11.5474 21.7401 11.518 21.635L9.937 15.5Z" fill="black" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.97942 1.25171L6.9585 1.30199L5.58662 4.60039C5.54342 4.70426 5.44573 4.77523 5.3336 4.78422L1.7727 5.0697L1.71841 5.07405L1.38687 5.10063L1.08608 5.12475C0.820085 5.14607 0.712228 5.47802 0.914889 5.65162L1.14406 5.84793L1.39666 6.06431L1.43802 6.09974L4.15105 8.42374C4.23648 8.49692 4.2738 8.61176 4.24769 8.72118L3.41882 12.196L3.40618 12.249L3.32901 12.5725L3.25899 12.866C3.19708 13.1256 3.47945 13.3308 3.70718 13.1917L3.9647 13.0344L4.24854 12.861L4.29502 12.8326L7.34365 10.9705C7.43965 10.9119 7.5604 10.9119 7.6564 10.9705L10.705 12.8326L10.7515 12.861L11.0354 13.0344L11.2929 13.1917C11.5206 13.3308 11.803 13.1256 11.7411 12.866L11.671 12.5725L11.5939 12.249L11.5812 12.196L10.7524 8.72118C10.7263 8.61176 10.7636 8.49692 10.849 8.42374L13.562 6.09974L13.6034 6.06431L13.856 5.84793L14.0852 5.65162C14.2878 5.47802 14.18 5.14607 13.914 5.12475L13.6132 5.10063L13.2816 5.07405L13.2274 5.0697L9.66645 4.78422C9.55432 4.77523 9.45663 4.70426 9.41343 4.60039L8.04155 1.30199L8.02064 1.25171L7.89291 0.944609L7.77702 0.665992C7.67454 0.419604 7.32551 0.419604 7.22303 0.665992L7.10715 0.944609L6.97942 1.25171ZM7.50003 2.60397L6.50994 4.98442C6.32273 5.43453 5.89944 5.74207 5.41351 5.78103L2.84361 5.98705L4.8016 7.66428C5.17183 7.98142 5.33351 8.47903 5.2204 8.95321L4.62221 11.461L6.8224 10.1171C7.23842 9.86302 7.76164 9.86302 8.17766 10.1171L10.3778 11.461L9.77965 8.95321C9.66654 8.47903 9.82822 7.98142 10.1984 7.66428L12.1564 5.98705L9.58654 5.78103C9.10061 5.74207 8.67732 5.43453 8.49011 4.98442L7.50003 2.60397Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.22303 0.665992C7.32551 0.419604 7.67454 0.419604 7.77702 0.665992L9.41343 4.60039C9.45663 4.70426 9.55432 4.77523 9.66645 4.78422L13.914 5.12475C14.18 5.14607 14.2878 5.47802 14.0852 5.65162L10.849 8.42374C10.7636 8.49692 10.7263 8.61176 10.7524 8.72118L11.7411 12.866C11.803 13.1256 11.5206 13.3308 11.2929 13.1917L7.6564 10.9705C7.5604 10.9119 7.43965 10.9119 7.34365 10.9705L3.70718 13.1917C3.47945 13.3308 3.19708 13.1256 3.25899 12.866L4.24769 8.72118C4.2738 8.61176 4.23648 8.49692 4.15105 8.42374L0.914889 5.65162C0.712228 5.47802 0.820086 5.14607 1.08608 5.12475L5.3336 4.78422C5.44573 4.77523 5.54342 4.70426 5.58662 4.60039L7.22303 0.665992Z" fill="currentColor"></path></svg>

Before

Width:  |  Height:  |  Size: 794 B

View File

@@ -1,3 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.88889 1H2.11111C1.49746 1 1 1.49746 1 2.11111V9.88889C1 10.5025 1.49746 11 2.11111 11H9.88889C10.5025 11 11 10.5025 11 9.88889V2.11111C11 1.49746 10.5025 1 9.88889 1Z" stroke="#C56757" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4L13 12" stroke="black" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 181 B

View File

@@ -1 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 11L6 4L10.5 7.5L6 11Z" fill="currentColor"></path></svg>

Before

Width:  |  Height:  |  Size: 164 B

View File

@@ -1,5 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1017 B

View File

@@ -28,6 +28,7 @@
"ctrl-0": "zed::ResetBufferFontSize",
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"alt-f9": "zed::Hide",
"f11": "zed::ToggleFullScreen"
}
},
@@ -51,11 +52,8 @@
// "alt-h": "editor::DeleteToPreviousWordStart",
// "alt-d": "editor::DeleteToNextWordEnd",
"ctrl-x": "editor::Cut",
"ctrl-insert": "editor::Copy",
"ctrl-c": "editor::Copy",
"shift-insert": "editor::Paste",
"ctrl-v": "editor::Paste",
"ctrl-y": "editor::Redo",
"ctrl-z": "editor::Undo",
"ctrl-shift-z": "editor::Redo",
"up": "editor::MoveUp",
@@ -191,25 +189,21 @@
"ctrl-shift-enter": "editor::NewlineBelow"
}
},
{
"context": "Markdown",
"bindings": {
"ctrl-c": "markdown::Copy"
}
},
{
"context": "AssistantPanel",
"bindings": {
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector"
"ctrl-shift-g": "search::SelectPrevMatch"
}
},
{
"context": "PromptLibrary",
"context": "ConversationEditor > Editor",
"bindings": {
"ctrl-n": "prompt_library::NewPrompt",
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole"
}
},
{
@@ -221,8 +215,7 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace",
"ctrl-l": "search::ToggleSelection"
"ctrl-h": "search::ToggleReplace"
}
},
{
@@ -286,7 +279,6 @@
"ctrl-alt-g": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
"ctrl-alt-shift-h": "search::ToggleReplace",
"ctrl-alt-shift-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
@@ -420,7 +412,7 @@
"alt-8": ["workspace::ActivatePane", 7],
"alt-9": ["workspace::ActivatePane", 8],
"ctrl-alt-b": "workspace::ToggleLeftDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-b": "workspace::ToggleRightDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-y": "workspace::CloseAllDocks",
"ctrl-shift-f": "pane::DeploySearch",
@@ -440,7 +432,6 @@
"ctrl-shift-p": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-?": "assistant::ToggleFocus",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
@@ -502,12 +493,6 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {
@@ -546,37 +531,12 @@
"ctrl-enter": "assistant::InlineAssist"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "ProjectSearchBar && !in_replace",
"bindings": {
"ctrl-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel",
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"ctrl-alt-c": "outline_panel::CopyPath",
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
"alt-ctrl-r": "outline_panel::RevealInFinder",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
}
},
{
"context": "ProjectPanel",
"bindings": {
@@ -586,22 +546,17 @@
"alt-ctrl-n": "project_panel::NewDirectory",
"ctrl-x": "project_panel::Cut",
"ctrl-c": "project_panel::Copy",
"ctrl-insert": "project_panel::Copy",
"ctrl-v": "project_panel::Paste",
"shift-insert": "project_panel::Paste",
"ctrl-alt-c": "project_panel::CopyPath",
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": "project_panel::Trash",
"delete": "project_panel::Trash",
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": true }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": true }],
"alt-ctrl-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"escape": "menu::Cancel"
"alt-shift-f": "project_panel::NewSearchInDirectory"
}
},
{
@@ -653,16 +608,19 @@
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"shift-ctrl-c": "terminal::Copy",
"ctrl-insert": "terminal::Copy",
"shift-ctrl-v": "terminal::Paste",
"shift-insert": "terminal::Paste",
"up": ["terminal::SendKeystroke", "up"],
"pageup": ["terminal::SendKeystroke", "pageup"],
"down": ["terminal::SendKeystroke", "down"],
"pagedown": ["terminal::SendKeystroke", "pagedown"],
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
// Some nice conveniences
"ctrl-backspace": ["terminal::SendText", "\u0015"],
"ctrl-right": ["terminal::SendText", "\u0005"],
"ctrl-left": ["terminal::SendText", "\u0001"]
}
}
]

View File

@@ -19,6 +19,9 @@
"cmd-escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"shift-enter": "picker::UseSelectedQuery",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"cmd-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"cmd-o": "workspace::Open",
@@ -176,12 +179,6 @@
"replace_enabled": true
}
],
"cmd-alt-l": [
"buffer_search::Deploy",
{
"selection_search_enabled": true
}
],
"cmd-e": [
"buffer_search::Deploy",
{
@@ -214,37 +211,28 @@
}
},
{
"context": "Markdown",
"context": "AssistantChat > Editor", // Used in the assistant2 crate
"bindings": {
"cmd-c": "markdown::Copy"
"enter": ["assistant2::Submit", "Simple"],
"cmd-enter": ["assistant2::Submit", "Codebase"],
"escape": "assistant2::Cancel"
}
},
{
"context": "AssistantPanel",
"context": "AssistantPanel", // Used in the assistant crate, which we're replacing
"bindings": {
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector"
"cmd-shift-g": "search::SelectPrevMatch"
}
},
{
"context": "ContextEditor > Editor",
"context": "ConversationEditor > Editor",
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "PromptLibrary",
"bindings": {
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
"cmd-w": "workspace::CloseWindow"
"ctrl-r": "assistant::CycleMessageRole"
}
},
{
@@ -256,8 +244,7 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"cmd-f": "search::FocusSearch",
"cmd-alt-f": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection"
"cmd-alt-f": "search::ToggleReplace"
}
},
{
@@ -323,7 +310,6 @@
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-h": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
@@ -475,7 +461,6 @@
"cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
@@ -535,12 +520,6 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {
@@ -585,19 +564,6 @@
"cmd-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel",
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"cmd-alt-c": "outline_panel::CopyPath",
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFinder",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
}
},
{
"context": "ProjectPanel",
"bindings": {
@@ -610,16 +576,14 @@
"cmd-v": "project_panel::Paste",
"cmd-alt-c": "project_panel::CopyPath",
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"backspace": "project_panel::Trash",
"delete": "project_panel::Trash",
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": true }],
"alt-cmd-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"escape": "menu::Cancel"
"alt-shift-f": "project_panel::NewSearchInDirectory"
}
},
{
@@ -666,14 +630,6 @@
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}
},
{
"context": "Picker",
"bindings": {
"f2": "picker::UseSelectedQuery",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
}
},
{
"context": "Terminal",
"bindings": {

View File

@@ -53,6 +53,7 @@
"cmd-alt-b": "editor::GoToDefinitionSplit",
"cmd-shift-b": "editor::GoToTypeDefinition",
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"alt-enter": "editor::ToggleCodeActions",
"f2": "editor::GoToDiagnostic",
"cmd-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
@@ -69,8 +70,7 @@
"cmd-f12": "outline::Toggle",
"cmd-7": "outline::Toggle",
"cmd-shift-o": "file_finder::Toggle",
"cmd-l": "go_to_line::Toggle",
"alt-enter": "editor::ToggleCodeActions"
"cmd-l": "go_to_line::Toggle"
}
},
{

View File

@@ -1,10 +1,4 @@
[
{
"context": "ProjectPanel || Editor",
"bindings": {
"ctrl-6": "pane::AlternateFile"
}
},
{
"context": "Editor && VimControl && !VimWaiting && !menu",
"bindings": {
@@ -80,7 +74,6 @@
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
"/": "vim::Search",
"g /": "pane::DeploySearch",
"?": [
"vim::Search",
{
@@ -124,9 +117,6 @@
}
}
],
"m": ["vim::PushOperator", "Mark"],
"'": ["vim::PushOperator", { "Jump": { "line": true } }],
"`": ["vim::PushOperator", { "Jump": { "line": false } }],
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"ctrl-o": "pane::GoBack",
@@ -156,7 +146,6 @@
"g shift-t": "pane::ActivatePrevItem",
"g d": "editor::GoToDefinition",
"g shift-d": "editor::GoToTypeDefinition",
"g cmd-d": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
@@ -246,12 +235,8 @@
"displayLines": true
}
],
"g v": "vim::RestoreVisualSelection",
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
"g i": "vim::InsertAtPrevious",
"g ,": "vim::ChangeListNewer",
"g ;": "vim::ChangeListOlder",
"shift-h": "vim::WindowTop",
"shift-m": "vim::WindowMiddle",
"shift-l": "vim::WindowBottom",
@@ -381,27 +366,18 @@
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
"\"": ["vim::PushOperator", "Register"],
"> >": "vim::Indent",
"< <": "vim::Outdent",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPrevDiagnostic",
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPrevHunk"
"] x": "editor::SelectSmallerSyntaxNode"
}
},
{
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
"\"": ["vim::PushOperator", "Register"],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
@@ -437,27 +413,6 @@
"d": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == gu",
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == gU",
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == g~",
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == d",
"bindings": {
@@ -487,18 +442,6 @@
"s": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == >",
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == <",
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
@@ -538,8 +481,8 @@
"shift-o": "vim::OtherEnd",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"shift-d": "vim::VisualDeleteLine",
"shift-x": "vim::VisualDeleteLine",
"shift-d": "vim::VisualDelete",
"shift-x": "vim::VisualDelete",
"y": "vim::VisualYank",
"shift-y": "vim::VisualYank",
"p": "vim::Paste",
@@ -608,7 +551,7 @@
}
},
{
"context": "Editor && vim_mode == normal && !VimWaiting",
"context": "Editor && vim_mode == normal",
"bindings": {
"g c c": "editor::ToggleComments"
}

View File

@@ -1,18 +1,5 @@
{
// The name of the Zed theme to use for the UI.
//
// The theme can also be set to follow system preferences:
//
// "theme": {
// "mode": "system",
// "light": "One Light",
// "dark": "One Dark"
// }
//
// Where `mode` is one of:
// - "system": Use the theme that corresponds to the system's appearance
// - "light": Use the theme indicated by the "light" field
// - "dark": Use the theme indicated by the "dark" field
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// The name of a base set of key bindings to use.
// This setting can take four values, each named after another
@@ -37,8 +24,6 @@
},
// The default font size for text in the editor
"buffer_font_size": 15,
// The weight of the editor font in standard CSS units from 100 to 900.
"buffer_font_weight": 400,
// Set the buffer's line height.
// May take 3 values:
// 1. Use a line height that's comfortable for reading (1.618)
@@ -57,8 +42,6 @@
// Disable ligatures:
"calt": false
},
// The weight of the UI font in standard CSS units from 100 to 900.
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
// The factor to grow the active pane by. Defaults to 1.0
@@ -88,28 +71,8 @@
"restore_on_startup": "last_workspace",
// Size of the drop target in the editor.
"drop_target_size": 0.2,
// Whether the window should be closed when using 'close active item' on a window with no tabs.
// May take 3 values:
// 1. Use the current platform's convention
// "when_closing_with_no_tabs": "platform_default"
// 2. Always close the window:
// "when_closing_with_no_tabs": "close_window",
// 3. Never close the window
// "when_closing_with_no_tabs": "keep_window_open",
"when_closing_with_no_tabs": "platform_default",
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// How to highlight the current line in the editor.
//
// 1. Don't highlight the current line:
// "none"
// 2. Highlight the gutter area:
// "gutter"
// 3. Highlight the editor area:
// "line"
// 4. Highlight the full line (default):
// "all"
"current_line_highlight": "all",
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
@@ -119,19 +82,24 @@
// The debounce delay before re-querying the language server for completion
// documentation when not included in original completion list.
"completion_documentation_secondary_query_debounce": 300,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if softwrap is set to 'preferred_line_length', and will show any
// additional guides as specified by the 'wrap_guides' setting.
// Whether to show wrap guides in the editor. Setting this to true will
// show a guide at the 'preferred_line_length' value if 'soft_wrap' is set to
// 'preferred_line_length', and will show any additional guides as specified
// by the 'wrap_guides' setting.
"show_wrap_guides": true,
// Character counts at which to show wrap guides in the editor.
"wrap_guides": [],
// Hide the values of in variables from visual display in private files
"redact_private_values": false,
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -157,19 +125,13 @@
// "none"
// 3. Draw all invisible symbols:
// "all"
// 4. Draw whitespaces at boundaries only:
// "boundaries"
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)
// - It is adjacent to a whitespace (left or right)
"show_whitespaces": "selection",
// Settings related to calls in Zed
"calls": {
// Join calls with the microphone live by default
"mute_on_join": false,
// Share your project when you are the first to join a channel
"share_on_join": false
"share_on_join": true
},
// Toolbar related settings
"toolbar": {
@@ -221,25 +183,6 @@
// Whether to show fold buttons in the gutter.
"folds": true
},
"indent_guides": {
/// Whether to show indent guides in the editor.
"enabled": true,
/// The width of the indent guides in pixels, between 1 and 10.
"line_width": 1,
/// Determines how indent guides are colored.
/// This setting can take the following three values:
///
/// 1. "disabled"
/// 2. "fixed"
/// 3. "indent_aware"
"coloring": "fixed",
/// Determines how indent guide backgrounds are colored.
/// This setting can take the following two values:
///
/// 1. "disabled"
/// 2. "indent_aware"
"background_coloring": "disabled"
},
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
// Scroll sensitivity multiplier. This multiplier is applied
@@ -295,29 +238,6 @@
/// when a directory has only one directory inside.
"auto_fold_dirs": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
"button": true,
// Default width of the outline panel.
"default_width": 300,
// Where to dock the outline panel. Can be 'left' or 'right'.
"dock": "left",
// Whether to show file icons in the outline panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the outline panel.
"folder_icons": true,
// Whether to show the git status in the outline panel.
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
// Whether to reveal it in the outline panel automatically,
// when a corresponding outline entry becomes active.
// Gitignored entries are never auto revealed.
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": true
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
"button": true,
@@ -363,27 +283,25 @@
// AI provider.
"provider": {
"name": "openai",
// The default model to use when creating new contexts. This
// The default model to use when starting new conversations. This
// setting can take three values:
//
// 1. "gpt-3.5-turbo"
// 2. "gpt-4"
// 3. "gpt-4-turbo-preview"
// 4. "gpt-4o"
"default_model": "gpt-4o"
"default_model": "gpt-4-turbo-preview"
}
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// Whether to perform linked edits of associated ranges, if the language server supports it.
// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
"linked_edits": true,
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.
"language_servers": ["..."],
"language_servers": [
"..."
],
// When to automatically save edited buffers. This setting can
// take four values.
//
@@ -398,8 +316,6 @@
"autosave": "off",
// Settings related to the editor's tab bar.
"tab_bar": {
// Whether or not to show the tab bar in the editor
"show": true,
// Whether or not to show the navigation history buttons.
"show_nav_history_buttons": true
},
@@ -431,8 +347,6 @@
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving
//
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
"format_on_save": "on",
// How to perform a buffer format. This setting can take 4 values:
//
@@ -456,13 +370,11 @@
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Prefer a single line generally, unless an overly long line is encountered.
// "soft_wrap": "prefer_line",
// 3. Soft wrap lines that overflow the editor:
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 4. Soft wrap lines at the preferred line length
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "prefer_line",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
@@ -478,8 +390,7 @@
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Automatically update Zed. This setting may be ignored on Linux if
// installed through a package manager.
// Automatically update Zed
"auto_update": true,
// Diagnostics configuration.
"diagnostics": {
@@ -519,7 +430,9 @@
"copilot": {
// The set of glob patterns for which copilot should be disabled
// in any matching file.
"disabled_globs": [".env"]
"disabled_globs": [
".env"
]
},
// Settings specific to journaling
"journal": {
@@ -548,7 +461,7 @@
// }
// }
"shell": "system",
// Where to dock terminals panel. Can be `left`, `right`, `bottom`.
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
"dock": "bottom",
// Default width when the terminal is docked to the left or right.
"default_width": 640,
@@ -630,8 +543,13 @@
// Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this
// in your project's settings, rather than globally.
"directories": [".env", "env", ".venv", "venv"],
// Can also be `csh`, `fish`, and `nushell`
"directories": [
".env",
"env",
".venv",
"venv"
],
// Can also be 'csh', 'fish', and `nushell`
"activate_script": "default"
}
},
@@ -656,7 +574,7 @@
// use those languages.
//
// For example, to treat files like `foo.notjs` as JavaScript,
// and `Embargo.lock` as TOML:
// and 'Embargo.lock' as TOML:
//
// {
// "JavaScript": ["notjs"],
@@ -673,147 +591,38 @@
},
// Different settings for specific languages.
"languages": {
"Astro": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-astro"]
}
},
"Blade": {
"prettier": {
"allowed": true
}
"C++": {
"format_on_save": "off"
},
"C": {
"format_on_save": "off"
},
"C++": {
"format_on_save": "off"
},
"CSS": {
"prettier": {
"allowed": true
}
},
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Gleam": {
"tab_size": 2
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
}
},
"GraphQL": {
"prettier": {
"allowed": true
}
},
"HEEX": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"HTML": {
"prettier": {
"allowed": true
}
"Make": {
"hard_tabs": true
},
"Java": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-java"]
}
},
"JavaScript": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}
},
"JSON": {
"prettier": {
"allowed": true
}
},
"Markdown": {
"format_on_save": "off",
"prettier": {
"allowed": true
}
},
"PHP": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"]
}
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "..."]
},
"SCSS": {
"prettier": {
"allowed": true
}
},
"SQL": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-sql"]
}
},
"Svelte": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-svelte"]
}
},
"TSX": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}
},
"Twig": {
"prettier": {
"allowed": true
}
},
"TypeScript": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}
},
"Vue.js": {
"prettier": {
"allowed": true
}
},
"XML": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-xml"]
}
},
"YAML": {
"prettier": {
"allowed": true
}
"Prisma": {
"tab_size": 2
}
},
// Zed's Prettier integration settings.
// Allows to enable/disable formatting with Prettier
// and configure default Prettier, used when no project-level Prettier installation is found.
// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
// project has no other Prettier installed.
"prettier": {
// // Whether to consider prettier formatter or not when attempting to format a file.
// "allowed": false,
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
// // the project has no other Prettier installed.
// "plugins": [],
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
// // the project has no other Prettier installed.
// Use regular Prettier json configuration:
// "trailingComma": "es5",
// "tabWidth": 4,
// "semi": false,
@@ -871,17 +680,5 @@
// - `short`: "2 s, 15 l, 32 c"
// - `long`: "2 selections, 15 lines, 32 characters"
// Default: long
"line_indicator_format": "long",
// Set a proxy to use. The proxy protocol is specified by the URI scheme.
//
// Supported URI scheme: `http`, `https`, `socks4`, `socks4a`, `socks5`,
// `socks5h`. `http` will be used when no scheme is specified.
//
// By default no proxy will be used, or Zed will try get proxy settings from
// environment variables.
//
// Examples:
// - "proxy": "socks5://localhost:10808"
// - "proxy": "http://127.0.0.1:10809"
"proxy": null
"line_indicator_format": "long"
}

View File

@@ -5,15 +5,6 @@
{
"name": "Gruvbox Dark",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -388,15 +379,6 @@
{
"name": "Gruvbox Dark Hard",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -771,15 +753,6 @@
{
"name": "Gruvbox Dark Soft",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -1154,15 +1127,6 @@
{
"name": "Gruvbox Light",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1537,15 +1501,6 @@
{
"name": "Gruvbox Light Hard",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1920,15 +1875,6 @@
{
"name": "Gruvbox Light Soft",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",

View File

@@ -281,14 +281,11 @@ impl ActivityIndicator {
message: "Installing Zed update…".to_string(),
on_click: None,
},
AutoUpdateStatus::Updated { binary_path } => Content {
AutoUpdateStatus::Updated => Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new({
let reload = workspace::Reload {
binary_path: Some(binary_path.clone()),
};
move |_, cx| workspace::reload(&reload, cx)
on_click: Some(Arc::new(|_, cx| {
workspace::restart(&Default::default(), cx)
})),
},
AutoUpdateStatus::Errored => Content {

View File

@@ -5,10 +5,6 @@ edition = "2021"
publish = false
license = "AGPL-3.0-or-later"
[features]
default = []
schemars = ["dep:schemars"]
[lints]
workspace = true
@@ -18,12 +14,9 @@ path = "src/anthropic.rs"
[dependencies]
anyhow.workspace = true
futures.workspace = true
http.workspace = true
isahc.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
util.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -1,22 +1,17 @@
use anyhow::{anyhow, Result};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, time::Duration};
use strum::EnumIter;
use std::{convert::TryFrom, sync::Arc};
use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
pub const ANTHROPIC_API_URL: &'static str = "https://api.anthropic.com";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub enum Model {
#[default]
#[serde(alias = "claude-3-opus", rename = "claude-3-opus-20240229")]
#[serde(rename = "claude-3-opus-20240229")]
Claude3Opus,
#[serde(alias = "claude-3-sonnet", rename = "claude-3-sonnet-20240229")]
#[serde(rename = "claude-3-sonnet-20240229")]
Claude3Sonnet,
#[serde(alias = "claude-3-haiku", rename = "claude-3-haiku-20240307")]
#[serde(rename = "claude-3-haiku-20240307")]
Claude3Haiku,
}
@@ -33,14 +28,6 @@ impl Model {
}
}
pub fn id(&self) -> &'static str {
match self {
Model::Claude3Opus => "claude-3-opus-20240229",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-opus-20240307",
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Claude3Opus => "Claude 3 Opus",
@@ -154,24 +141,20 @@ pub enum TextDelta {
}
pub async fn stream_completion(
client: &dyn HttpClient,
client: Arc<dyn HttpClient>,
api_url: &str,
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let uri = format!("{api_url}/v1/messages");
let mut request_builder = HttpRequest::builder()
let request = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", "tools-2024-04-04")
.header("Anthropic-Beta", "messages-2023-12-15")
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
}
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
.header("Content-Type", "application/json")
.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
let reader = BufReader::new(response.into_body());
@@ -213,7 +196,7 @@ pub async fn stream_completion(
// #[cfg(test)]
// mod tests {
// use super::*;
// use http::IsahcHttpClient;
// use util::http::IsahcHttpClient;
// #[tokio::test]
// async fn stream_completion_success() {

View File

@@ -16,9 +16,9 @@ use rust_embed::RustEmbed;
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
fn load(&self, path: &str) -> Result<std::borrow::Cow<'static, [u8]>> {
Self::get(path)
.map(|f| Some(f.data))
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
@@ -42,10 +42,7 @@ impl Assets {
let mut embedded_fonts = Vec::new();
for font_path in font_paths {
if font_path.ends_with(".ttf") {
let font_bytes = cx
.asset_source()
.load(&font_path)?
.expect("Assets should never return None");
let font_bytes = cx.asset_source().load(&font_path)?;
embedded_fonts.push(font_bytes);
}
}

View File

@@ -5,18 +5,12 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant.rs"
doctest = false
[dependencies]
anyhow.workspace = true
anthropic = { workspace = true, features = ["schemars"] }
assistant_slash_command.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
@@ -25,43 +19,30 @@ editor.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http.workspace = true
indoc.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
ordered-float.workspace = true
parking_lot.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
rustdoc.workspace = true
schemars.workspace = true
search.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
similar.workspace = true
smol.workspace = true
strsim = "0.11"
strum.workspace = true
telemetry_events.workspace = true
theme.workspace = true
tiktoken-rs.workspace = true
toml.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
picker.workspace = true
[dev-dependencies]
ctor.workspace = true
@@ -70,4 +51,3 @@ env_logger.workspace = true
log.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
unindent.workspace = true

View File

@@ -0,0 +1,3 @@
Push content to a deeper layer.
A context can have multiple sublayers.
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.

View File

@@ -1,44 +1,32 @@
pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
mod context_store;
mod inline_assistant;
mod model_selector;
mod prompt_library;
mod prompts;
mod search;
mod slash_command;
mod saved_conversation;
mod streaming_diff;
pub use assistant_panel::AssistantPanel;
mod embedded_scope;
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
use assistant_slash_command::SlashCommandRegistry;
pub use assistant_panel::AssistantPanel;
use assistant_settings::{AssistantSettings, OpenAiModel, ZedDotDevModel};
use chrono::{DateTime, Local};
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
pub(crate) use context_store::*;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use inline_assistant::*;
pub(crate) use model_selector::*;
use rustdoc::RustdocStore;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use gpui::{actions, AppContext, BorrowAppContext, Global, SharedString};
pub(crate) use saved_conversation::*;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use slash_command::{
active_command, default_command, diagnostics_command, fetch_command, file_command, now_command,
project_command, prompt_command, rustdoc_command, search_command, tabs_command,
};
use std::{
fmt::{self, Display},
sync::Arc,
};
pub(crate) use streaming_diff::*;
use util::paths::EMBEDDINGS_DIR;
actions!(
assistant,
[
NewConversation,
Assist,
Split,
CycleMessageRole,
@@ -46,11 +34,7 @@ actions!(
ToggleFocus,
ResetKey,
InlineAssist,
InsertActivePrompt,
ToggleHistory,
ApplyEdit,
ConfirmCommand,
ToggleModelSelector
ToggleIncludeConversation,
]
);
@@ -89,15 +73,13 @@ impl Display for Role {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum LanguageModel {
Cloud(CloudModel),
ZedDotDev(ZedDotDevModel),
OpenAi(OpenAiModel),
Anthropic(AnthropicModel),
Ollama(OllamaModel),
}
impl Default for LanguageModel {
fn default() -> Self {
LanguageModel::Cloud(CloudModel::default())
LanguageModel::ZedDotDev(ZedDotDevModel::default())
}
}
@@ -105,36 +87,28 @@ impl LanguageModel {
pub fn telemetry_id(&self) -> String {
match self {
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
LanguageModel::Cloud(model) => format!("zed.dev/{}", model.id()),
LanguageModel::Ollama(model) => format!("ollama/{}", model.id()),
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.id()),
}
}
pub fn display_name(&self) -> String {
match self {
LanguageModel::OpenAi(model) => model.display_name().into(),
LanguageModel::Anthropic(model) => model.display_name().into(),
LanguageModel::Cloud(model) => model.display_name().into(),
LanguageModel::Ollama(model) => model.display_name().into(),
LanguageModel::OpenAi(model) => format!("openai/{}", model.display_name()),
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.display_name()),
}
}
pub fn max_token_count(&self) -> usize {
match self {
LanguageModel::OpenAi(model) => model.max_token_count(),
LanguageModel::Anthropic(model) => model.max_token_count(),
LanguageModel::Cloud(model) => model.max_token_count(),
LanguageModel::Ollama(model) => model.max_token_count(),
LanguageModel::ZedDotDev(model) => model.max_token_count(),
}
}
pub fn id(&self) -> &str {
match self {
LanguageModel::OpenAi(model) => model.id(),
LanguageModel::Anthropic(model) => model.id(),
LanguageModel::Cloud(model) => model.id(),
LanguageModel::Ollama(model) => model.id(),
LanguageModel::ZedDotDev(model) => model.id(),
}
}
}
@@ -179,21 +153,6 @@ impl LanguageModelRequest {
tools: Vec::new(),
}
}
/// Before we send the request to the server, we can perform fixups on it appropriate to the model.
pub fn preprocess(&mut self) {
match &self.model {
LanguageModel::OpenAi(_) => {}
LanguageModel::Anthropic(_) => {}
LanguageModel::Ollama(_) => {}
LanguageModel::Cloud(model) => match model {
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku => {
preprocess_anthropic_request(self);
}
_ => {}
},
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -219,6 +178,7 @@ pub struct LanguageModelChoiceDelta {
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
sent_at: DateTime<Local>,
status: MessageStatus,
}
@@ -265,62 +225,27 @@ impl Assistant {
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
cx.spawn(|mut cx| {
let client = client.clone();
async move {
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticIndex::new(
EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
prompt_library::init(cx);
completion_provider::init(client.clone(), cx);
assistant_slash_command::init(cx);
register_slash_commands(cx);
completion_provider::init(client, cx);
assistant_panel::init(cx);
inline_assistant::init(client.telemetry().clone(), cx);
RustdocStore::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
});
Assistant::update_global(cx, |assistant, cx| {
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
cx.observe_global::<SettingsStore>(|cx| {
Assistant::update_global(cx, |assistant, cx| {
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
})
.detach();
}
fn register_slash_commands(cx: &mut AppContext) {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, true);
slash_command_registry.register_command(diagnostics_command::DiagnosticsCommand, true);
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,6 @@
use std::fmt;
pub use anthropic::Model as AnthropicModel;
use gpui::Pixels;
pub use ollama::Model as OllamaModel;
pub use open_ai::Model as OpenAiModel;
use schemars::{
schema::{InstanceType, Metadata, Schema, SchemaObject},
@@ -13,24 +11,20 @@ use serde::{
Deserialize, Deserializer, Serialize, Serializer,
};
use settings::{Settings, SettingsSources};
use strum::{EnumIter, IntoEnumIterator};
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
pub enum CloudModel {
#[derive(Clone, Debug, Default, PartialEq)]
pub enum ZedDotDevModel {
Gpt3Point5Turbo,
Gpt4,
Gpt4Turbo,
#[default]
Gpt4Omni,
Gpt4Turbo,
Claude3Opus,
Claude3Sonnet,
Claude3Haiku,
Custom(String),
}
impl Serialize for CloudModel {
impl Serialize for ZedDotDevModel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -39,7 +33,7 @@ impl Serialize for CloudModel {
}
}
impl<'de> Deserialize<'de> for CloudModel {
impl<'de> Deserialize<'de> for ZedDotDevModel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -47,7 +41,7 @@ impl<'de> Deserialize<'de> for CloudModel {
struct ZedDotDevModelVisitor;
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
type Value = CloudModel;
type Value = ZedDotDevModel;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
@@ -57,10 +51,12 @@ impl<'de> Deserialize<'de> for CloudModel {
where
E: de::Error,
{
let model = CloudModel::iter()
.find(|model| model.id() == value)
.unwrap_or_else(|| CloudModel::Custom(value.to_string()));
Ok(model)
match value {
"gpt-3.5-turbo" => Ok(ZedDotDevModel::Gpt3Point5Turbo),
"gpt-4" => Ok(ZedDotDevModel::Gpt4),
"gpt-4-turbo-preview" => Ok(ZedDotDevModel::Gpt4Turbo),
_ => Ok(ZedDotDevModel::Custom(value.to_owned())),
}
}
}
@@ -68,29 +64,29 @@ impl<'de> Deserialize<'de> for CloudModel {
}
}
impl JsonSchema for CloudModel {
impl JsonSchema for ZedDotDevModel {
fn schema_name() -> String {
"ZedDotDevModel".to_owned()
}
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
let variants = CloudModel::iter()
.filter_map(|model| {
let id = model.id();
if id.is_empty() {
None
} else {
Some(id.to_string())
}
})
.collect::<Vec<_>>();
let variants = vec![
"gpt-3.5-turbo".to_owned(),
"gpt-4".to_owned(),
"gpt-4-turbo-preview".to_owned(),
];
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
enum_values: Some(variants.into_iter().map(|s| s.into()).collect()),
metadata: Some(Box::new(Metadata {
title: Some("ZedDotDevModel".to_owned()),
default: Some(CloudModel::default().id().into()),
examples: variants.into_iter().map(Into::into).collect(),
default: Some(serde_json::json!("gpt-4-turbo-preview")),
examples: vec![
serde_json::json!("gpt-3.5-turbo"),
serde_json::json!("gpt-4"),
serde_json::json!("gpt-4-turbo-preview"),
serde_json::json!("custom-model-name"),
],
..Default::default()
})),
..Default::default()
@@ -98,13 +94,12 @@ impl JsonSchema for CloudModel {
}
}
impl CloudModel {
impl ZedDotDevModel {
pub fn id(&self) -> &str {
match self {
Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
Self::Gpt4 => "gpt-4",
Self::Gpt4Turbo => "gpt-4-turbo-preview",
Self::Gpt4Omni => "gpt-4o",
Self::Claude3Opus => "claude-3-opus",
Self::Claude3Sonnet => "claude-3-sonnet",
Self::Claude3Haiku => "claude-3-haiku",
@@ -117,7 +112,6 @@ impl CloudModel {
Self::Gpt3Point5Turbo => "GPT 3.5 Turbo",
Self::Gpt4 => "GPT 4",
Self::Gpt4Turbo => "GPT 4 Turbo",
Self::Gpt4Omni => "GPT 4 Omni",
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
@@ -129,20 +123,11 @@ impl CloudModel {
match self {
Self::Gpt3Point5Turbo => 2048,
Self::Gpt4 => 4096,
Self::Gpt4Turbo | Self::Gpt4Omni => 128000,
Self::Gpt4Turbo => 128000,
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 200000,
Self::Custom(_) => 4096, // TODO: Make this configurable
}
}
pub fn preprocess_request(&self, request: &mut LanguageModelRequest) {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => {
preprocess_anthropic_request(request)
}
_ => {}
}
}
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
@@ -154,64 +139,36 @@ pub enum AssistantDockPosition {
Bottom,
}
#[derive(Debug, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProvider {
#[serde(rename = "zed.dev")]
ZedDotDev {
model: CloudModel,
#[serde(default)]
default_model: ZedDotDevModel,
},
#[serde(rename = "openai")]
OpenAi {
model: OpenAiModel,
#[serde(default)]
default_model: OpenAiModel,
#[serde(default = "open_ai_url")]
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
},
Anthropic {
model: AnthropicModel,
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
},
Ollama {
model: OllamaModel,
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
},
}
impl Default for AssistantProvider {
fn default() -> Self {
Self::OpenAi {
model: OpenAiModel::default(),
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
Self::ZedDotDev {
default_model: ZedDotDevModel::default(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContent {
#[serde(rename = "zed.dev")]
ZedDotDev { default_model: Option<CloudModel> },
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "anthropic")]
Anthropic {
default_model: Option<AnthropicModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "ollama")]
Ollama {
default_model: Option<OllamaModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
fn open_ai_url() -> String {
"https://api.openai.com/v1".into()
}
#[derive(Debug, Default)]
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct AssistantSettings {
pub enabled: bool,
pub button: bool,
@@ -262,17 +219,15 @@ impl AssistantSettingsContent {
default_width: settings.default_width,
default_height: settings.default_height,
provider: if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
Some(AssistantProviderContent::OpenAi {
default_model: settings.default_open_ai_model.clone(),
api_url: Some(open_ai_api_url.clone()),
low_speed_timeout_in_seconds: None,
Some(AssistantProvider::OpenAi {
default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
api_url: open_ai_api_url.clone(),
})
} else {
settings.default_open_ai_model.clone().map(|open_ai_model| {
AssistantProviderContent::OpenAi {
default_model: Some(open_ai_model),
api_url: None,
low_speed_timeout_in_seconds: None,
AssistantProvider::OpenAi {
default_model: open_ai_model,
api_url: open_ai_url(),
}
})
},
@@ -292,71 +247,6 @@ impl AssistantSettingsContent {
}
}
}
pub fn set_model(&mut self, new_model: LanguageModel) {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => match &mut settings.provider {
Some(AssistantProviderContent::ZedDotDev {
default_model: model,
}) => {
if let LanguageModel::Cloud(new_model) = new_model {
*model = Some(new_model);
}
}
Some(AssistantProviderContent::OpenAi {
default_model: model,
..
}) => {
if let LanguageModel::OpenAi(new_model) = new_model {
*model = Some(new_model);
}
}
Some(AssistantProviderContent::Anthropic {
default_model: model,
..
}) => {
if let LanguageModel::Anthropic(new_model) = new_model {
*model = Some(new_model);
}
}
provider => match new_model {
LanguageModel::Cloud(model) => {
*provider = Some(AssistantProviderContent::ZedDotDev {
default_model: Some(model),
})
}
LanguageModel::OpenAi(model) => {
*provider = Some(AssistantProviderContent::OpenAi {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
LanguageModel::Anthropic(model) => {
*provider = Some(AssistantProviderContent::Anthropic {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
LanguageModel::Ollama(model) => {
*provider = Some(AssistantProviderContent::Ollama {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
},
},
},
AssistantSettingsContent::Legacy(settings) => {
if let LanguageModel::OpenAi(model) = new_model {
settings.default_open_ai_model = Some(model);
}
}
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -405,7 +295,7 @@ pub struct AssistantSettingsContentV1 {
///
/// This can either be the internal `zed.dev` service or an external `openai` service,
/// each with their respective default models and configurations.
provider: Option<AssistantProviderContent>,
provider: Option<AssistantProvider>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -426,11 +316,11 @@ pub struct LegacyAssistantSettingsContent {
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when creating new contexts.
/// The default OpenAI model to use when starting new conversations.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when creating new contexts.
/// OpenAI API base URL to use when starting new conversations.
///
/// Default: https://api.openai.com/v1
pub openai_api_url: Option<String>,
@@ -463,112 +353,28 @@ impl Settings for AssistantSettings {
if let Some(provider) = value.provider.clone() {
match (&mut settings.provider, provider) {
(
AssistantProvider::ZedDotDev { model },
AssistantProviderContent::ZedDotDev {
default_model: model_override,
AssistantProvider::ZedDotDev { default_model },
AssistantProvider::ZedDotDev {
default_model: default_model_override,
},
) => {
merge(model, model_override);
*default_model = default_model_override;
}
(
AssistantProvider::OpenAi {
model,
default_model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProviderContent::OpenAi {
default_model: model_override,
AssistantProvider::OpenAi {
default_model: default_model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
*default_model = default_model_override;
*api_url = api_url_override;
}
(
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Ollama {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
}
(
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Anthropic {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
}
(provider, provider_override) => {
*provider = match provider_override {
AssistantProviderContent::ZedDotDev {
default_model: model,
} => AssistantProvider::ZedDotDev {
model: model.unwrap_or_default(),
},
AssistantProviderContent::OpenAi {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::OpenAi {
model: model.unwrap_or_default(),
api_url: api_url.unwrap_or_else(|| open_ai::OPEN_AI_API_URL.into()),
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Anthropic {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::Anthropic {
model: model.unwrap_or_default(),
api_url: api_url
.unwrap_or_else(|| anthropic::ANTHROPIC_API_URL.into()),
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Ollama {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::Ollama {
model: model.unwrap_or_default(),
api_url: api_url.unwrap_or_else(|| ollama::OLLAMA_API_URL.into()),
low_speed_timeout_in_seconds,
},
};
(merged, provider_override) => {
*merged = provider_override;
}
}
}
@@ -578,7 +384,7 @@ impl Settings for AssistantSettings {
}
}
fn merge<T>(target: &mut T, value: Option<T>) {
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
@@ -586,7 +392,7 @@ fn merge<T>(target: &mut T, value: Option<T>) {
#[cfg(test)]
mod tests {
use gpui::{AppContext, UpdateGlobal};
use gpui::{AppContext, BorrowAppContext};
use settings::SettingsStore;
use super::*;
@@ -601,14 +407,13 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
model: OpenAiModel::FourOmni,
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
default_model: OpenAiModel::FourTurbo,
api_url: open_ai_url()
}
);
// Ensure backward-compatibility.
SettingsStore::update_global(cx, |store, cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{
@@ -623,12 +428,11 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
model: OpenAiModel::FourOmni,
api_url: "test-url".into(),
low_speed_timeout_in_seconds: None,
default_model: OpenAiModel::FourTurbo,
api_url: "test-url".into()
}
);
SettingsStore::update_global(cx, |store, cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{
@@ -643,14 +447,13 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
model: OpenAiModel::Four,
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
default_model: OpenAiModel::Four,
api_url: open_ai_url()
}
);
// The new version supports setting a custom model when using zed.dev.
SettingsStore::update_global(cx, |store, cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{
@@ -669,7 +472,7 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::ZedDotDev {
model: CloudModel::Custom("custom".into())
default_model: ZedDotDevModel::Custom("custom".into())
}
);
}

View File

@@ -0,0 +1,660 @@
use crate::{
streaming_diff::{Hunk, StreamingDiff},
CompletionProvider, LanguageModelRequest,
};
use anyhow::Result;
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{EventEmitter, Model, ModelContext, Task};
use language::{Rope, TransactionId};
use std::{cmp, future, ops::Range};
pub enum Event {
Finished,
Undone,
}
#[derive(Clone)]
pub enum CodegenKind {
Transform { range: Range<Anchor> },
Generate { position: Anchor },
}
pub struct Codegen {
buffer: Model<MultiBuffer>,
snapshot: MultiBufferSnapshot,
kind: CodegenKind,
last_equal_ranges: Vec<Range<Anchor>>,
transaction_id: Option<TransactionId>,
error: Option<anyhow::Error>,
generation: Task<()>,
idle: bool,
_subscription: gpui::Subscription,
}
impl EventEmitter<Event> for Codegen {}
impl Codegen {
pub fn new(buffer: Model<MultiBuffer>, kind: CodegenKind, cx: &mut ModelContext<Self>) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
Self {
buffer: buffer.clone(),
snapshot,
kind,
last_equal_ranges: Default::default(),
transaction_id: Default::default(),
error: Default::default(),
idle: true,
generation: Task::ready(()),
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
}
}
fn handle_buffer_event(
&mut self,
_buffer: Model<MultiBuffer>,
event: &multi_buffer::Event,
cx: &mut ModelContext<Self>,
) {
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
if self.transaction_id == Some(*transaction_id) {
self.transaction_id = None;
self.generation = Task::ready(());
cx.emit(Event::Undone);
}
}
}
pub fn range(&self) -> Range<Anchor> {
match &self.kind {
CodegenKind::Transform { range } => range.clone(),
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
}
}
pub fn kind(&self) -> &CodegenKind {
&self.kind
}
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
&self.last_equal_ranges
}
pub fn idle(&self) -> bool {
self.idle
}
pub fn error(&self) -> Option<&anyhow::Error> {
self.error.as_ref()
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
let range = self.range();
let snapshot = self.snapshot.clone();
let selected_text = snapshot
.text_for_range(range.start..range.end)
.collect::<Rope>();
let selection_start = range.start.to_point(&snapshot);
let suggested_line_indent = snapshot
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
.into_values()
.next()
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
let response = CompletionProvider::global(cx).complete(prompt);
self.generation = cx.spawn(|this, mut cx| {
async move {
let generate = async {
let mut edit_start = range.start.to_offset(&snapshot);
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
let diff = cx.background_executor().spawn(async move {
let chunks = strip_invalid_spans_from_codeblock(response.await?);
futures::pin_mut!(chunks);
let mut diff = StreamingDiff::new(selected_text.to_string());
let mut new_text = String::new();
let mut base_indent = None;
let mut line_indent = None;
let mut first_line = true;
while let Some(chunk) = chunks.next().await {
let chunk = chunk?;
let mut lines = chunk.split('\n').peekable();
while let Some(line) = lines.next() {
new_text.push_str(line);
if line_indent.is_none() {
if let Some(non_whitespace_ch_ix) =
new_text.find(|ch: char| !ch.is_whitespace())
{
line_indent = Some(non_whitespace_ch_ix);
base_indent = base_indent.or(line_indent);
let line_indent = line_indent.unwrap();
let base_indent = base_indent.unwrap();
let indent_delta = line_indent as i32 - base_indent as i32;
let mut corrected_indent_len = cmp::max(
0,
suggested_line_indent.len as i32 + indent_delta,
)
as usize;
if first_line {
corrected_indent_len = corrected_indent_len
.saturating_sub(selection_start.column as usize);
}
let indent_char = suggested_line_indent.char();
let mut indent_buffer = [0; 4];
let indent_str =
indent_char.encode_utf8(&mut indent_buffer);
new_text.replace_range(
..line_indent,
&indent_str.repeat(corrected_indent_len),
);
}
}
if line_indent.is_some() {
hunks_tx.send(diff.push_new(&new_text)).await?;
new_text.clear();
}
if lines.peek().is_some() {
hunks_tx.send(diff.push_new("\n")).await?;
line_indent = None;
first_line = false;
}
}
}
hunks_tx.send(diff.push_new(&new_text)).await?;
hunks_tx.send(diff.finish()).await?;
anyhow::Ok(())
});
while let Some(hunks) = hunks_rx.next().await {
this.update(&mut cx, |this, cx| {
this.last_equal_ranges.clear();
let transaction = this.buffer.update(cx, |buffer, cx| {
// Avoid grouping assistant edits with user edits.
buffer.finalize_last_transaction(cx);
buffer.start_transaction(cx);
buffer.edit(
hunks.into_iter().filter_map(|hunk| match hunk {
Hunk::Insert { text } => {
let edit_start = snapshot.anchor_after(edit_start);
Some((edit_start..edit_start, text))
}
Hunk::Remove { len } => {
let edit_end = edit_start + len;
let edit_range = snapshot.anchor_after(edit_start)
..snapshot.anchor_before(edit_end);
edit_start = edit_end;
Some((edit_range, String::new()))
}
Hunk::Keep { len } => {
let edit_end = edit_start + len;
let edit_range = snapshot.anchor_after(edit_start)
..snapshot.anchor_before(edit_end);
edit_start = edit_end;
this.last_equal_ranges.push(edit_range);
None
}
}),
None,
cx,
);
buffer.end_transaction(cx)
});
if let Some(transaction) = transaction {
if let Some(first_transaction) = this.transaction_id {
// Group all assistant edits into the first transaction.
this.buffer.update(cx, |buffer, cx| {
buffer.merge_transactions(
transaction,
first_transaction,
cx,
)
});
} else {
this.transaction_id = Some(transaction);
this.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx)
});
}
}
cx.notify();
})?;
}
diff.await?;
anyhow::Ok(())
};
let result = generate.await;
this.update(&mut cx, |this, cx| {
this.last_equal_ranges.clear();
this.idle = true;
if let Err(error) = result {
this.error = Some(error);
}
cx.emit(Event::Finished);
cx.notify();
})
.ok();
}
});
self.error.take();
self.idle = false;
cx.notify();
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction_id) = self.transaction_id {
self.buffer
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
}
}
}
fn strip_invalid_spans_from_codeblock(
stream: impl Stream<Item = Result<String>>,
) -> impl Stream<Item = Result<String>> {
let mut first_line = true;
let mut buffer = String::new();
let mut starts_with_markdown_codeblock = false;
let mut includes_start_or_end_span = false;
stream.filter_map(move |chunk| {
let chunk = match chunk {
Ok(chunk) => chunk,
Err(err) => return future::ready(Some(Err(err))),
};
buffer.push_str(&chunk);
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
includes_start_or_end_span = true;
buffer = buffer
.strip_prefix("<|S|>")
.or_else(|| buffer.strip_prefix("<|S|"))
.unwrap_or(&buffer)
.to_string();
} else if buffer.ends_with("|E|>") {
includes_start_or_end_span = true;
} else if buffer.starts_with("<|")
|| buffer.starts_with("<|S")
|| buffer.starts_with("<|S|")
|| buffer.ends_with('|')
|| buffer.ends_with("|E")
|| buffer.ends_with("|E|")
{
return future::ready(None);
}
if first_line {
if buffer.is_empty() || buffer == "`" || buffer == "``" {
return future::ready(None);
} else if buffer.starts_with("```") {
starts_with_markdown_codeblock = true;
if let Some(newline_ix) = buffer.find('\n') {
buffer.replace_range(..newline_ix + 1, "");
first_line = false;
} else {
return future::ready(None);
}
}
}
let mut text = buffer.to_string();
if starts_with_markdown_codeblock {
text = text
.strip_suffix("\n```\n")
.or_else(|| text.strip_suffix("\n```"))
.or_else(|| text.strip_suffix("\n``"))
.or_else(|| text.strip_suffix("\n`"))
.or_else(|| text.strip_suffix('\n'))
.unwrap_or(&text)
.to_string();
}
if includes_start_or_end_span {
text = text
.strip_suffix("|E|>")
.or_else(|| text.strip_suffix("E|>"))
.or_else(|| text.strip_prefix("|>"))
.or_else(|| text.strip_prefix('>'))
.unwrap_or(&text)
.to_string();
};
if text.contains('\n') {
first_line = false;
}
let remainder = buffer.split_off(text.len());
let result = if buffer.is_empty() {
None
} else {
Some(Ok(buffer.clone()))
};
buffer = remainder;
future::ready(result)
})
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::FakeCompletionProvider;
use super::*;
use futures::stream::{self};
use gpui::{Context, TestAppContext};
use indoc::indoc;
use language::{
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
Point,
};
use rand::prelude::*;
use serde::Serialize;
use settings::SettingsStore;
#[derive(Serialize)]
pub struct DummyCompletionRequest {
pub name: String,
}
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
let provider = FakeCompletionProvider::default();
cx.set_global(cx.update(SettingsStore::test));
cx.set_global(CompletionProvider::Fake(provider.clone()));
cx.update(language_settings::init);
let text = indoc! {"
fn main() {
let x = 0;
for _ in 0..10 {
x += 1;
}
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let codegen =
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Transform { range }, cx));
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
let mut new_text = concat!(
" let mut x = 0;\n",
" while x < 10 {\n",
" x += 1;\n",
" }",
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test(iterations = 10)]
async fn test_autoindent_when_generating_past_indentation(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
let provider = FakeCompletionProvider::default();
cx.set_global(CompletionProvider::Fake(provider.clone()));
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
let text = indoc! {"
fn main() {
le
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let position = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 6))
});
let codegen =
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Generate { position }, cx));
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
let mut new_text = concat!(
"t mut x = 0;\n",
"while x < 10 {\n",
" x += 1;\n",
"}", //
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test(iterations = 10)]
async fn test_autoindent_when_generating_before_indentation(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
let provider = FakeCompletionProvider::default();
cx.set_global(CompletionProvider::Fake(provider.clone()));
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
let text = concat!(
"fn main() {\n",
" \n",
"}\n" //
);
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let position = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 2))
});
let codegen =
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Generate { position }, cx));
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
let mut new_text = concat!(
"let mut x = 0;\n",
"while x < 10 {\n",
" x += 1;\n",
"}", //
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test]
async fn test_strip_invalid_spans_from_codeblock() {
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks(
"```html\n```js\nLorem ipsum dolor\n```\n```",
2
))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"```js\nLorem ipsum dolor\n```"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"``\nLorem ipsum dolor\n```"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum"
);
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
stream::iter(
text.chars()
.collect::<Vec<_>>()
.chunks(size)
.map(|chunk| Ok(chunk.iter().collect::<String>()))
.collect::<Vec<_>>(),
)
}
}
fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_indents_query(
r#"
(call_expression) @indent
(field_expression) @indent
(_ "(" ")" @end) @indent
(_ "{" "}" @end) @indent
"#,
)
.unwrap()
}
}

View File

@@ -1,16 +1,12 @@
mod anthropic;
mod cloud;
#[cfg(test)]
mod fake;
mod ollama;
mod open_ai;
mod zed;
pub use anthropic::*;
pub use cloud::*;
#[cfg(test)]
pub use fake::*;
pub use ollama::*;
pub use open_ai::*;
pub use zed::*;
use crate::{
assistant_settings::{AssistantProvider, AssistantSettings},
@@ -22,48 +18,27 @@ use futures::{future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
use settings::{Settings, SettingsStore};
use std::sync::Arc;
use std::time::Duration;
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
let mut settings_version = 0;
let provider = match &AssistantSettings::get_global(cx).provider {
AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud(
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
),
AssistantProvider::ZedDotDev { default_model } => {
CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
default_model.clone(),
client.clone(),
settings_version,
cx,
))
}
AssistantProvider::OpenAi {
model,
default_model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
model.clone(),
default_model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Ollama(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
)),
};
cx.set_global(provider);
@@ -74,111 +49,42 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
CompletionProvider::OpenAi(provider),
AssistantProvider::OpenAi {
model,
default_model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
);
provider.update(default_model.clone(), api_url.clone(), settings_version);
}
(
CompletionProvider::Anthropic(provider),
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
},
CompletionProvider::ZedDotDev(provider),
AssistantProvider::ZedDotDev { default_model },
) => {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
);
provider.update(default_model.clone(), settings_version);
}
(
CompletionProvider::Ollama(provider),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
);
}
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
provider.update(model.clone(), settings_version);
}
(_, AssistantProvider::ZedDotDev { model }) => {
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
model.clone(),
(CompletionProvider::OpenAi(_), AssistantProvider::ZedDotDev { default_model }) => {
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
default_model.clone(),
client.clone(),
settings_version,
cx,
));
}
(
_,
CompletionProvider::ZedDotDev(_),
AssistantProvider::OpenAi {
model,
default_model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
model.clone(),
default_model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
));
}
(
_,
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
));
}
(
_,
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Ollama(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
));
}
#[cfg(test)]
(CompletionProvider::Fake(_), _) => unimplemented!(),
}
})
})
@@ -187,11 +93,9 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
pub enum CompletionProvider {
OpenAi(OpenAiCompletionProvider),
Anthropic(AnthropicCompletionProvider),
Cloud(CloudCompletionProvider),
ZedDotDev(ZedDotDevCompletionProvider),
#[cfg(test)]
Fake(FakeCompletionProvider),
Ollama(OllamaCompletionProvider),
}
impl gpui::Global for CompletionProvider {}
@@ -201,35 +105,10 @@ impl CompletionProvider {
cx.global::<Self>()
}
pub fn available_models(&self) -> Vec<LanguageModel> {
match self {
CompletionProvider::OpenAi(provider) => provider
.available_models()
.map(LanguageModel::OpenAi)
.collect(),
CompletionProvider::Anthropic(provider) => provider
.available_models()
.map(LanguageModel::Anthropic)
.collect(),
CompletionProvider::Cloud(provider) => provider
.available_models()
.map(LanguageModel::Cloud)
.collect(),
CompletionProvider::Ollama(provider) => provider
.available_models()
.map(|model| LanguageModel::Ollama(model.clone()))
.collect(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
}
pub fn settings_version(&self) -> usize {
match self {
CompletionProvider::OpenAi(provider) => provider.settings_version(),
CompletionProvider::Anthropic(provider) => provider.settings_version(),
CompletionProvider::Cloud(provider) => provider.settings_version(),
CompletionProvider::Ollama(provider) => provider.settings_version(),
CompletionProvider::ZedDotDev(provider) => provider.settings_version(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -238,9 +117,7 @@ impl CompletionProvider {
pub fn is_authenticated(&self) -> bool {
match self {
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
CompletionProvider::Ollama(provider) => provider.is_authenticated(),
CompletionProvider::ZedDotDev(provider) => provider.is_authenticated(),
#[cfg(test)]
CompletionProvider::Fake(_) => true,
}
@@ -249,9 +126,7 @@ impl CompletionProvider {
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
match self {
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
CompletionProvider::Ollama(provider) => provider.authenticate(cx),
CompletionProvider::ZedDotDev(provider) => provider.authenticate(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
@@ -260,9 +135,7 @@ impl CompletionProvider {
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
match self {
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx),
CompletionProvider::ZedDotDev(provider) => provider.authentication_prompt(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -271,22 +144,20 @@ impl CompletionProvider {
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
match self {
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
CompletionProvider::Ollama(provider) => provider.reset_credentials(cx),
CompletionProvider::ZedDotDev(_) => Task::ready(Ok(())),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
}
pub fn model(&self) -> LanguageModel {
pub fn default_model(&self) -> LanguageModel {
match self {
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
CompletionProvider::Ollama(provider) => LanguageModel::Ollama(provider.model()),
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.default_model()),
CompletionProvider::ZedDotDev(provider) => {
LanguageModel::ZedDotDev(provider.default_model())
}
#[cfg(test)]
CompletionProvider::Fake(_) => LanguageModel::default(),
CompletionProvider::Fake(_) => unimplemented!(),
}
}
@@ -297,11 +168,9 @@ impl CompletionProvider {
) -> BoxFuture<'static, Result<usize>> {
match self {
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
CompletionProvider::Ollama(provider) => provider.count_tokens(request, cx),
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
#[cfg(test)]
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
CompletionProvider::Fake(_) => unimplemented!(),
}
}
@@ -311,9 +180,7 @@ impl CompletionProvider {
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
match self {
CompletionProvider::OpenAi(provider) => provider.complete(request),
CompletionProvider::Anthropic(provider) => provider.complete(request),
CompletionProvider::Cloud(provider) => provider.complete(request),
CompletionProvider::Ollama(provider) => provider.complete(request),
CompletionProvider::ZedDotDev(provider) => provider.complete(request),
#[cfg(test)]
CompletionProvider::Fake(provider) => provider.complete(),
}

View File

@@ -1,359 +0,0 @@
use crate::{
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
Role,
};
use crate::{count_open_ai_tokens, LanguageModelRequestMessage};
use anthropic::{stream_completion, Request, RequestMessage};
use anyhow::{anyhow, Result};
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, FontStyle, Task, TextStyle, View, WhiteSpace};
use http::HttpClient;
use settings::Settings;
use std::time::Duration;
use std::{env, sync::Arc};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::prelude::*;
use util::ResultExt;
pub struct AnthropicCompletionProvider {
api_key: Option<String>,
api_url: String,
model: AnthropicModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
}
impl AnthropicCompletionProvider {
pub fn new(
model: AnthropicModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) -> Self {
Self {
api_key: None,
api_url,
model,
http_client,
low_speed_timeout,
settings_version,
}
}
pub fn update(
&mut self,
model: AnthropicModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.model = model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = AnthropicModel> {
AnthropicModel::iter()
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
let api_url = self.api_url.clone();
cx.spawn(|mut cx| async move {
let api_key = if let Ok(api_key) = env::var("ANTHROPIC_API_KEY") {
api_key
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
.await?
.ok_or_else(|| anyhow!("credentials not found"))?;
String::from_utf8(api_key)?
};
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.api_key = Some(api_key);
}
})
})
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
let delete_credentials = cx.delete_credentials(&self.api_url);
cx.spawn(|mut cx| async move {
delete_credentials.await.log_err();
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.api_key = None;
}
})
})
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx))
.into()
}
pub fn model(&self) -> AnthropicModel {
self.model.clone()
}
pub fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
count_open_ai_tokens(request, cx.background_executor())
}
pub fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = self.to_anthropic_request(request);
let http_client = self.http_client.clone();
let api_key = self.api_key.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let api_key = api_key.ok_or_else(|| anyhow!("missing api key"))?;
let request = stream_completion(
http_client.as_ref(),
&api_url,
&api_key,
request,
low_speed_timeout,
);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(response) => match response {
anthropic::ResponseEvent::ContentBlockStart {
content_block, ..
} => match content_block {
anthropic::ContentBlock::Text { text } => Some(Ok(text)),
},
anthropic::ResponseEvent::ContentBlockDelta { delta, .. } => {
match delta {
anthropic::TextDelta::TextDelta { text } => Some(Ok(text)),
}
}
_ => None,
},
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
preprocess_anthropic_request(&mut request);
let model = match request.model {
LanguageModel::Anthropic(model) => model,
_ => self.model(),
};
let mut system_message = String::new();
if request
.messages
.first()
.map_or(false, |message| message.role == Role::System)
{
system_message = request.messages.remove(0).content;
}
Request {
model,
messages: request
.messages
.iter()
.map(|msg| RequestMessage {
role: match msg.role {
Role::User => anthropic::Role::User,
Role::Assistant => anthropic::Role::Assistant,
Role::System => unreachable!("filtered out by preprocess_request"),
},
content: msg.content.clone(),
})
.collect(),
stream: true,
system: system_message,
max_tokens: 4092,
}
}
}
pub fn preprocess_anthropic_request(request: &mut LanguageModelRequest) {
let mut new_messages: Vec<LanguageModelRequestMessage> = Vec::new();
let mut system_message = String::new();
for message in request.messages.drain(..) {
if message.content.is_empty() {
continue;
}
match message.role {
Role::User | Role::Assistant => {
if let Some(last_message) = new_messages.last_mut() {
if last_message.role == message.role {
last_message.content.push_str("\n\n");
last_message.content.push_str(&message.content);
continue;
}
}
new_messages.push(message);
}
Role::System => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.content);
}
}
}
if !system_message.is_empty() {
request.messages.insert(
0,
LanguageModelRequestMessage {
role: Role::System,
content: system_message,
},
);
}
request.messages = new_messages;
}
struct AuthenticationPrompt {
api_key: View<Editor>,
api_url: String,
}
impl AuthenticationPrompt {
fn new(api_url: String, cx: &mut WindowContext) -> Self {
Self {
api_key: cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text(
"sk-000000000000000000000000000000000000000000000000",
cx,
);
editor
}),
api_url,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let api_key = self.api_key.read(cx).text(cx);
if api_key.is_empty() {
return;
}
let write_credentials = cx.write_credentials(&self.api_url, "Bearer", api_key.as_bytes());
cx.spawn(|_, mut cx| async move {
write_credentials.await?;
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.api_key = Some(api_key);
}
})
})
.detach_and_log_err(cx);
}
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
&self.api_key,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
}
impl Render for AuthenticationPrompt {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const INSTRUCTIONS: [&str; 4] = [
"To use the assistant panel or inline assistant, you need to add your Anthropic API key.",
"You can create an API key at: https://console.anthropic.com/settings/keys",
"",
"Paste your Anthropic API key below and hit enter to use the assistant:",
];
v_flex()
.p_4()
.size_full()
.on_action(cx.listener(Self::save_api_key))
.children(
INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
)
.child(
h_flex()
.w_full()
.my_2()
.px_2()
.py_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(self.render_api_key_editor(cx)),
)
.child(
Label::new(
"You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.",
)
.size(LabelSize::Small),
)
.child(
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),
)
.into_any()
}
}

View File

@@ -1,348 +0,0 @@
use crate::{
assistant_settings::OllamaModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
use anyhow::Result;
use futures::StreamExt as _;
use futures::{future::BoxFuture, stream::BoxStream, FutureExt};
use gpui::{AnyView, AppContext, Task};
use http::HttpClient;
use ollama::{
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
Role as OllamaRole,
};
use std::sync::Arc;
use std::time::Duration;
use ui::{prelude::*, ButtonLike, ElevationIndex};
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
pub struct OllamaCompletionProvider {
api_url: String,
model: OllamaModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
available_models: Vec<OllamaModel>,
}
impl OllamaCompletionProvider {
pub fn new(
model: OllamaModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
cx: &AppContext,
) -> Self {
cx.spawn({
let api_url = api_url.clone();
let client = http_client.clone();
let model = model.name.clone();
|_| async move {
if model.is_empty() {
return Ok(());
}
preload_model(client.as_ref(), &api_url, &model).await
}
})
.detach_and_log_err(cx);
Self {
api_url,
model,
http_client,
low_speed_timeout,
settings_version,
available_models: Default::default(),
}
}
pub fn update(
&mut self,
model: OllamaModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
cx: &AppContext,
) {
cx.spawn({
let api_url = api_url.clone();
let client = self.http_client.clone();
let model = model.name.clone();
|_| async move { preload_model(client.as_ref(), &api_url, &model).await }
})
.detach_and_log_err(cx);
if model.name.is_empty() {
self.select_first_available_model()
} else {
self.model = model;
}
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = &OllamaModel> {
self.available_models.iter()
}
pub fn select_first_available_model(&mut self) {
if let Some(model) = self.available_models.first() {
self.model = model.clone();
}
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn is_authenticated(&self) -> bool {
!self.available_models.is_empty()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
self.fetch_models(cx)
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
self.fetch_models(cx)
}
pub fn fetch_models(&self, cx: &AppContext) -> Task<Result<()>> {
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
// As a proxy for the server being "authenticated", we'll check if its up by fetching the models
cx.spawn(|mut cx| async move {
let models = get_models(http_client.as_ref(), &api_url, None).await?;
let mut models: Vec<OllamaModel> = models
.into_iter()
// Since there is no metadata from the Ollama API
// indicating which models are embedding models,
// simply filter out models with "-embed" in their name
.filter(|model| !model.name.contains("-embed"))
.map(|model| OllamaModel::new(&model.name))
.collect();
models.sort_by(|a, b| a.name.cmp(&b.name));
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Ollama(provider) = provider {
provider.available_models = models;
if !provider.available_models.is_empty() && provider.model.name.is_empty() {
provider.select_first_available_model()
}
}
})
})
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
let fetch_models = Box::new(move |cx: &mut WindowContext| {
cx.update_global::<CompletionProvider, _>(|provider, cx| {
if let CompletionProvider::Ollama(provider) = provider {
provider.fetch_models(cx)
} else {
Task::ready(Ok(()))
}
})
});
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
.into()
}
pub fn model(&self) -> OllamaModel {
self.model.clone()
}
pub fn count_tokens(
&self,
request: LanguageModelRequest,
_cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
// There is no endpoint for this _yet_ in Ollama
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
let token_count = request
.messages
.iter()
.map(|msg| msg.content.chars().count())
.sum::<usize>()
/ 4;
async move { Ok(token_count) }.boxed()
}
pub fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = self.to_ollama_request(request);
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let request =
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(delta) => {
let content = match delta.message {
ChatMessage::User { content } => content,
ChatMessage::Assistant { content } => content,
ChatMessage::System { content } => content,
};
Some(Ok(content))
}
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
let model = match request.model {
LanguageModel::Ollama(model) => model,
_ => self.model(),
};
ChatRequest {
model: model.name,
messages: request
.messages
.into_iter()
.map(|msg| match msg.role {
Role::User => ChatMessage::User {
content: msg.content,
},
Role::Assistant => ChatMessage::Assistant {
content: msg.content,
},
Role::System => ChatMessage::System {
content: msg.content,
},
})
.collect(),
keep_alive: model.keep_alive.unwrap_or_default(),
stream: true,
options: Some(ChatOptions {
num_ctx: Some(model.max_tokens),
stop: Some(request.stop),
temperature: Some(request.temperature),
..Default::default()
}),
}
}
}
impl From<Role> for ollama::Role {
fn from(val: Role) -> Self {
match val {
Role::User => OllamaRole::User,
Role::Assistant => OllamaRole::Assistant,
Role::System => OllamaRole::System,
}
}
}
struct DownloadOllamaMessage {
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
}
impl DownloadOllamaMessage {
pub fn new(
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
_cx: &mut ViewContext<Self>,
) -> Self {
Self { retry_connection }
}
fn render_download_button(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
ButtonLike::new("download_ollama_button")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Get Ollama"))
.on_click(move |_, cx| cx.open_url(OLLAMA_DOWNLOAD_URL))
}
fn render_retry_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
ButtonLike::new("retry_ollama_models")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Retry"))
.on_click(cx.listener(move |this, _, cx| {
let connected = (this.retry_connection)(cx);
cx.spawn(|_this, _cx| async move {
connected.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}))
}
fn render_next_steps(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.p_4()
.size_full()
.gap_2()
.child(
Label::new("Once Ollama is on your machine, make sure to download a model or two.")
.size(LabelSize::Large),
)
.child(
h_flex().w_full().p_4().justify_center().gap_2().child(
ButtonLike::new("view-models")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("View Available Models"))
.on_click(move |_, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
),
)
}
}
impl Render for DownloadOllamaMessage {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.p_4()
.size_full()
.gap_2()
.child(Label::new("To use Ollama models via the assistant, Ollama must be running on your machine with at least one model downloaded.").size(LabelSize::Large))
.child(
h_flex()
.w_full()
.p_4()
.justify_center()
.gap_2()
.child(
self.render_download_button(cx)
)
.child(
self.render_retry_button(cx)
)
)
.child(self.render_next_steps(cx))
.into_any()
}
}

View File

@@ -1,65 +1,47 @@
use crate::assistant_settings::CloudModel;
use crate::{
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
use anyhow::{anyhow, Result};
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, FontStyle, Task, TextStyle, View, WhiteSpace};
use http::HttpClient;
use gpui::{AnyView, AppContext, FontStyle, FontWeight, Task, TextStyle, View, WhiteSpace};
use open_ai::{stream_completion, Request, RequestMessage, Role as OpenAiRole};
use settings::Settings;
use std::time::Duration;
use std::{env, sync::Arc};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::prelude::*;
use util::ResultExt;
use util::{http::HttpClient, ResultExt};
pub struct OpenAiCompletionProvider {
api_key: Option<String>,
api_url: String,
model: OpenAiModel,
default_model: OpenAiModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
}
impl OpenAiCompletionProvider {
pub fn new(
model: OpenAiModel,
default_model: OpenAiModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) -> Self {
Self {
api_key: None,
api_url,
model,
default_model,
http_client,
low_speed_timeout,
settings_version,
}
}
pub fn update(
&mut self,
model: OpenAiModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.model = model;
pub fn update(&mut self, default_model: OpenAiModel, api_url: String, settings_version: usize) {
self.default_model = default_model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = OpenAiModel> {
OpenAiModel::iter()
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
@@ -109,8 +91,8 @@ impl OpenAiCompletionProvider {
.into()
}
pub fn model(&self) -> OpenAiModel {
self.model.clone()
pub fn default_model(&self) -> OpenAiModel {
self.default_model.clone()
}
pub fn count_tokens(
@@ -130,16 +112,9 @@ impl OpenAiCompletionProvider {
let http_client = self.http_client.clone();
let api_key = self.api_key.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let api_key = api_key.ok_or_else(|| anyhow!("missing api key"))?;
let request = stream_completion(
http_client.as_ref(),
&api_url,
&api_key,
request,
low_speed_timeout,
);
let request = stream_completion(http_client.as_ref(), &api_url, &api_key, request);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
@@ -156,8 +131,8 @@ impl OpenAiCompletionProvider {
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::ZedDotDev(_) => self.default_model(),
LanguageModel::OpenAi(model) => model,
_ => self.model(),
};
Request {
@@ -208,17 +183,7 @@ pub fn count_open_ai_tokens(
})
.collect::<Vec<_>>();
match request.model {
LanguageModel::Anthropic(_)
| LanguageModel::Cloud(CloudModel::Claude3Opus)
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
// Tiktoken doesn't yet support these models, so we manually use the
// same tokenizer as GPT-4.
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
}
_ => tiktoken_rs::num_tokens_from_messages(request.model.id(), &messages),
}
tiktoken_rs::num_tokens_from_messages(request.model.id(), &messages)
})
.boxed()
}
@@ -278,7 +243,7 @@ impl AuthenticationPrompt {
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: settings.ui_font.weight,
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,
@@ -336,7 +301,7 @@ impl Render for AuthenticationPrompt {
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),

View File

@@ -1,5 +1,5 @@
use crate::{
assistant_settings::CloudModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
assistant_settings::ZedDotDevModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
LanguageModelRequest,
};
use anyhow::{anyhow, Result};
@@ -7,20 +7,19 @@ use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryFutureExt};
use gpui::{AnyView, AppContext, Task};
use std::{future, sync::Arc};
use strum::IntoEnumIterator;
use ui::prelude::*;
pub struct CloudCompletionProvider {
pub struct ZedDotDevCompletionProvider {
client: Arc<Client>,
model: CloudModel,
default_model: ZedDotDevModel,
settings_version: usize,
status: client::Status,
_maintain_client_status: Task<()>,
}
impl CloudCompletionProvider {
impl ZedDotDevCompletionProvider {
pub fn new(
model: CloudModel,
default_model: ZedDotDevModel,
client: Arc<Client>,
settings_version: usize,
cx: &mut AppContext,
@@ -30,7 +29,7 @@ impl CloudCompletionProvider {
let maintain_client_status = cx.spawn(|mut cx| async move {
while let Some(status) = status_rx.next().await {
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Cloud(provider) = provider {
if let CompletionProvider::ZedDotDev(provider) = provider {
provider.status = status;
} else {
unreachable!()
@@ -40,39 +39,24 @@ impl CloudCompletionProvider {
});
Self {
client,
model,
default_model,
settings_version,
status,
_maintain_client_status: maintain_client_status,
}
}
pub fn update(&mut self, model: CloudModel, settings_version: usize) {
self.model = model;
pub fn update(&mut self, default_model: ZedDotDevModel, settings_version: usize) {
self.default_model = default_model;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = CloudModel> {
let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() {
Some(custom_model)
} else {
None
};
CloudModel::iter().filter_map(move |model| {
if let CloudModel::Custom(_) = model {
Some(CloudModel::Custom(custom_model.take()?))
} else {
Some(model)
}
})
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn model(&self) -> CloudModel {
self.model.clone()
pub fn default_model(&self) -> ZedDotDevModel {
self.default_model.clone()
}
pub fn is_authenticated(&self) -> bool {
@@ -94,19 +78,21 @@ impl CloudCompletionProvider {
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
match request.model {
LanguageModel::Cloud(CloudModel::Gpt4)
| LanguageModel::Cloud(CloudModel::Gpt4Turbo)
| LanguageModel::Cloud(CloudModel::Gpt4Omni)
| LanguageModel::Cloud(CloudModel::Gpt3Point5Turbo) => {
LanguageModel::OpenAi(_) => future::ready(Err(anyhow!("invalid model"))).boxed(),
LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Turbo)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt3Point5Turbo) => {
count_open_ai_tokens(request, cx.background_executor())
}
LanguageModel::Cloud(
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku,
LanguageModel::ZedDotDev(
ZedDotDevModel::Claude3Opus
| ZedDotDevModel::Claude3Sonnet
| ZedDotDevModel::Claude3Haiku,
) => {
// Can't find a tokenizer for Claude 3, so for now just use the same as OpenAI's as an approximation.
count_open_ai_tokens(request, cx.background_executor())
}
LanguageModel::Cloud(CloudModel::Custom(model)) => {
LanguageModel::ZedDotDev(ZedDotDevModel::Custom(model)) => {
let request = self.client.request(proto::CountTokensWithLanguageModel {
model,
messages: request
@@ -121,16 +107,13 @@ impl CloudCompletionProvider {
}
.boxed()
}
_ => future::ready(Err(anyhow!("invalid model"))).boxed(),
}
}
pub fn complete(
&self,
mut request: LanguageModelRequest,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
request.preprocess();
let request = proto::CompleteWithLanguageModel {
model: request.model.id().to_string(),
messages: request

View File

@@ -1,196 +0,0 @@
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
use anyhow::{anyhow, Result};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, Model, ModelContext, Task};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
use ui::Context;
use util::{paths::CONTEXTS_DIR, ResultExt, TryFutureExt};
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub start: usize,
}
#[derive(Serialize, Deserialize)]
pub struct SavedContext {
pub id: Option<String>,
pub zed: String,
pub version: String,
pub text: String,
pub messages: Vec<SavedMessage>,
pub message_metadata: HashMap<MessageId, MessageMetadata>,
pub summary: String,
}
impl SavedContext {
pub const VERSION: &'static str = "0.2.0";
}
#[derive(Serialize, Deserialize)]
struct SavedContextV0_1_0 {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
api_url: Option<String>,
model: OpenAiModel,
}
#[derive(Clone)]
pub struct SavedContextMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}
pub struct ContextStore {
contexts_metadata: Vec<SavedContextMetadata>,
fs: Arc<dyn Fs>,
_watch_updates: Task<Option<()>>,
}
impl ContextStore {
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(&CONTEXTS_DIR, CONTEXT_WATCH_DURATION).await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
contexts_metadata: Vec::new(),
fs,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
}
anyhow::Ok(())
}
.log_err()
}),
})?;
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
Ok(this)
})
}
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
let saved_context = fs.load(&path).await?;
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
match saved_context_json
.get("version")
.ok_or_else(|| anyhow!("version not found"))?
{
serde_json::Value::String(version) => match version.as_str() {
SavedContext::VERSION => {
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
}
"0.1.0" => {
let saved_context =
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
Ok(SavedContext {
id: saved_context.id,
zed: saved_context.zed,
version: saved_context.version,
text: saved_context.text,
messages: saved_context.messages,
message_metadata: saved_context.message_metadata,
summary: saved_context.summary,
})
}
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
},
_ => Err(anyhow!("version not found on saved context")),
}
})
}
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
let metadata = self.contexts_metadata.clone();
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
if query.is_empty() {
metadata
} else {
let candidates = metadata
.iter()
.enumerate()
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| metadata[mat.candidate_id].clone())
.collect()
}
})
}
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
fs.create_dir(&CONTEXTS_DIR).await?;
let mut paths = fs.read_dir(&CONTEXTS_DIR).await?;
let mut contexts = Vec::<SavedContextMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out contexts saved by the new assistant.
if !re.is_match(file_name) {
continue;
}
if let Some(title) = re.replace(file_name, "").lines().next() {
contexts.push(SavedContextMetadata {
title: title.to_string(),
path,
mtime: metadata.mtime.into(),
});
}
}
}
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
this.update(&mut cx, |this, cx| {
this.contexts_metadata = contexts;
cx.notify();
})
})
}
}

View File

@@ -0,0 +1,91 @@
use editor::MultiBuffer;
use gpui::{AppContext, Model, ModelContext, Subscription};
use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
#[derive(Default)]
pub struct EmbeddedScope {
active_buffer: Option<Model<MultiBuffer>>,
active_buffer_enabled: bool,
active_buffer_subscription: Option<Subscription>,
}
impl EmbeddedScope {
pub fn new() -> Self {
Self {
active_buffer: None,
active_buffer_enabled: true,
active_buffer_subscription: None,
}
}
pub fn set_active_buffer(
&mut self,
buffer: Option<Model<MultiBuffer>>,
cx: &mut ModelContext<Conversation>,
) {
self.active_buffer_subscription.take();
if let Some(active_buffer) = buffer.clone() {
self.active_buffer_subscription =
Some(cx.subscribe(&active_buffer, |conversation, _, e, cx| {
if let multi_buffer::Event::Edited { .. } = e {
conversation.count_remaining_tokens(cx)
}
}));
}
self.active_buffer = buffer;
}
pub fn active_buffer(&self) -> Option<&Model<MultiBuffer>> {
self.active_buffer.as_ref()
}
pub fn active_buffer_enabled(&self) -> bool {
self.active_buffer_enabled
}
pub fn set_active_buffer_enabled(&mut self, enabled: bool) {
self.active_buffer_enabled = enabled;
}
/// Provide a message for the language model based on the active buffer.
pub fn message(&self, cx: &AppContext) -> Option<LanguageModelRequestMessage> {
if !self.active_buffer_enabled {
return None;
}
let active_buffer = self.active_buffer.as_ref()?;
let buffer = active_buffer.read(cx);
if let Some(singleton) = buffer.as_singleton() {
let singleton = singleton.read(cx);
let filename = singleton
.file()
.map(|file| file.path().to_string_lossy())
.unwrap_or("Untitled".into());
let text = singleton.text();
let language = singleton
.language()
.map(|l| {
let name = l.code_fence_block_name();
name.to_string()
})
.unwrap_or_default();
let markdown =
format!("User's active file `{filename}`:\n\n```{language}\n{text}```\n\n");
return Some(LanguageModelRequestMessage {
role: Role::System,
content: markdown,
});
}
None
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +0,0 @@
use std::sync::Arc;
use crate::{assistant_settings::AssistantSettings, CompletionProvider, ToggleModelSelector};
use fs::Fs;
use settings::update_settings_file;
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, PopoverMenuHandle, Tooltip};
#[derive(IntoElement)]
pub struct ModelSelector {
handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
}
impl ModelSelector {
pub fn new(handle: PopoverMenuHandle<ContextMenu>, fs: Arc<dyn Fs>) -> Self {
ModelSelector { handle, fs }
}
}
impl RenderOnce for ModelSelector {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
popover_menu("model-switcher")
.with_handle(self.handle)
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::global(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();
move |_| Label::new(model.display_name()).into_any_element()
},
{
let fs = self.fs.clone();
let model = model.clone();
move |cx| {
let model = model.clone();
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings| settings.set_model(model),
);
}
},
);
}
menu
})
.into()
})
.trigger(
ButtonLike::new("active-model")
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(
Label::new(
CompletionProvider::global(cx).model().display_name(),
)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
div().child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| {
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
}),
)
.anchor(gpui::AnchorCorner::BottomRight)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
use anyhow::{anyhow, Result};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
cmp::Reverse,
ffi::OsStr,
path::{Path, PathBuf},
sync::Arc,
};
use util::paths::CONVERSATIONS_DIR;
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub start: usize,
}
#[derive(Serialize, Deserialize)]
pub struct SavedConversation {
pub id: Option<String>,
pub zed: String,
pub version: String,
pub text: String,
pub messages: Vec<SavedMessage>,
pub message_metadata: HashMap<MessageId, MessageMetadata>,
pub summary: String,
}
impl SavedConversation {
pub const VERSION: &'static str = "0.2.0";
pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
let saved_conversation = fs.load(path).await?;
let saved_conversation_json =
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
match saved_conversation_json
.get("version")
.ok_or_else(|| anyhow!("version not found"))?
{
serde_json::Value::String(version) => match version.as_str() {
Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
"0.1.0" => {
let saved_conversation =
serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
Ok(Self {
id: saved_conversation.id,
zed: saved_conversation.zed,
version: saved_conversation.version,
text: saved_conversation.text,
messages: saved_conversation.messages,
message_metadata: saved_conversation.message_metadata,
summary: saved_conversation.summary,
})
}
_ => Err(anyhow!(
"unrecognized saved conversation version: {}",
version
)),
},
_ => Err(anyhow!("version not found on saved conversation")),
}
}
}
#[derive(Serialize, Deserialize)]
struct SavedConversationV0_1_0 {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
api_url: Option<String>,
model: OpenAiModel,
}
pub struct SavedConversationMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}

View File

@@ -1,171 +0,0 @@
use language::Rope;
use std::ops::Range;
/// Search the given buffer for the given substring, ignoring any differences
/// in line indentation between the query and the buffer.
///
/// Returns a vector of ranges of byte offsets in the buffer corresponding
/// to the entire lines of the buffer.
pub fn fuzzy_search_lines(haystack: &Rope, needle: &str) -> Option<Range<usize>> {
const SIMILARITY_THRESHOLD: f64 = 0.8;
let mut best_match: Option<(Range<usize>, f64)> = None; // (range, score)
let mut haystack_lines = haystack.chunks().lines();
let mut haystack_line_start = 0;
while let Some(mut haystack_line) = haystack_lines.next() {
let next_haystack_line_start = haystack_line_start + haystack_line.len() + 1;
let mut advanced_to_next_haystack_line = false;
let mut matched = true;
let match_start = haystack_line_start;
let mut match_end = next_haystack_line_start;
let mut match_score = 0.0;
let mut needle_lines = needle.lines().peekable();
while let Some(needle_line) = needle_lines.next() {
let similarity = line_similarity(haystack_line, needle_line);
if similarity >= SIMILARITY_THRESHOLD {
match_end = haystack_lines.offset();
match_score += similarity;
if needle_lines.peek().is_some() {
if let Some(next_haystack_line) = haystack_lines.next() {
advanced_to_next_haystack_line = true;
haystack_line = next_haystack_line;
} else {
matched = false;
break;
}
} else {
break;
}
} else {
matched = false;
break;
}
}
if matched
&& best_match
.as_ref()
.map(|(_, best_score)| match_score > *best_score)
.unwrap_or(true)
{
best_match = Some((match_start..match_end, match_score));
}
if advanced_to_next_haystack_line {
haystack_lines.seek(next_haystack_line_start);
}
haystack_line_start = next_haystack_line_start;
}
best_match.map(|(range, _)| range)
}
/// Calculates the similarity between two lines, ignoring leading and trailing whitespace,
/// using the Jaro-Winkler distance.
///
/// Returns a value between 0.0 and 1.0, where 1.0 indicates an exact match.
fn line_similarity(line1: &str, line2: &str) -> f64 {
strsim::jaro_winkler(line1.trim(), line2.trim())
}
#[cfg(test)]
mod test {
use super::*;
use gpui::{AppContext, Context as _};
use language::Buffer;
use unindent::Unindent as _;
use util::test::marked_text_ranges;
#[gpui::test]
fn test_fuzzy_search_lines(cx: &mut AppContext) {
let (text, expected_ranges) = marked_text_ranges(
&r#"
fn main() {
if a() {
assert_eq!(
1 + 2,
does_not_match,
);
}
println!("hi");
assert_eq!(
1 + 2,
3,
); // this last line does not match
« assert_eq!(
1 + 2,
3,
);
»
« assert_eq!(
"something",
"else",
);
»
}
"#
.unindent(),
false,
);
let buffer = cx.new_model(|cx| Buffer::local(&text, cx));
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let actual_range = fuzzy_search_lines(
snapshot.as_rope(),
&"
assert_eq!(
1 + 2,
3,
);
"
.unindent(),
)
.unwrap();
assert_eq!(actual_range, expected_ranges[0]);
let actual_range = fuzzy_search_lines(
snapshot.as_rope(),
&"
assert_eq!(
1 + 2,
3,
);
"
.unindent(),
)
.unwrap();
assert_eq!(actual_range, expected_ranges[0]);
let actual_range = fuzzy_search_lines(
snapshot.as_rope(),
&"
asst_eq!(
\"something\",
\"els\"
)
"
.unindent(),
)
.unwrap();
assert_eq!(actual_range, expected_ranges[1]);
let actual_range = fuzzy_search_lines(
snapshot.as_rope(),
&"
assert_eq!(
2 + 1,
3,
);
"
.unindent(),
);
assert_eq!(actual_range, None);
}
}

View File

@@ -1,349 +0,0 @@
use crate::assistant_panel::ContextEditor;
use anyhow::Result;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use rope::Point;
use std::{
ops::Range,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use workspace::Workspace;
pub mod active_command;
pub mod default_command;
pub mod diagnostics_command;
pub mod fetch_command;
pub mod file_command;
pub mod now_command;
pub mod project_command;
pub mod prompt_command;
pub mod rustdoc_command;
pub mod search_command;
pub mod tabs_command;
pub(crate) struct SlashCommandCompletionProvider {
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
}
pub(crate) struct SlashCommandLine {
/// The range within the line containing the command name.
pub name: Range<usize>,
/// The range within the line containing the command argument.
pub argument: Option<Range<usize>>,
}
impl SlashCommandCompletionProvider {
pub fn new(
commands: Arc<SlashCommandRegistry>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
editor,
commands,
workspace,
}
}
fn complete_command_name(
&self,
command_name: &str,
command_range: Range<Anchor>,
name_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let candidates = self
.commands
.command_names()
.into_iter()
.enumerate()
.map(|(ix, def)| StringMatchCandidate {
id: ix,
string: def.to_string(),
char_bag: def.as_ref().into(),
})
.collect::<Vec<_>>();
let commands = self.commands.clone();
let command_name = command_name.to_string();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
cx.spawn(|mut cx| async move {
let matches = match_strings(
&candidates,
&command_name,
true,
usize::MAX,
&Default::default(),
cx.background_executor().clone(),
)
.await;
cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| {
let command = commands.command(&mat.string)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
if requires_argument {
new_text.push(' ');
}
let confirm = editor.clone().zip(workspace.clone()).and_then(
|(editor, workspace)| {
(!requires_argument).then(|| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
true,
workspace.clone(),
cx,
);
})
.ok();
}) as Arc<_>
})
},
);
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
new_text,
label: command.label(cx),
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: requires_argument,
confirm,
})
})
.collect()
})
})
}
fn complete_command_argument(
&self,
command_name: &str,
argument: String,
command_range: Range<Anchor>,
argument_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
if let Some(command) = self.commands.command(command_name) {
let completions = command.complete_argument(
argument,
new_cancel_flag.clone(),
self.workspace.clone(),
cx,
);
let command_name: Arc<str> = command_name.into();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
cx.background_executor().spawn(async move {
Ok(completions
.await?
.into_iter()
.map(|command_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let command_range = command_range.clone();
let command_name = command_name.clone();
let command_argument = command_argument.clone();
move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&command_argument),
true,
workspace.clone(),
cx,
);
})
.ok();
}
}) as Arc<_>
});
project::Completion {
old_range: argument_range.clone(),
label: CodeLabel::plain(command_argument.clone(), None),
new_text: command_argument.clone(),
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: false,
confirm,
}
})
.collect())
})
} else {
cx.background_executor()
.spawn(async move { Ok(Vec::new()) })
}
}
}
impl CompletionProvider for SlashCommandCompletionProvider {
fn completions(
&self,
buffer: &Model<Buffer>,
buffer_position: Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, argument, command_range, argument_range)) =
buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
let call = SlashCommandLine::parse(line)?;
let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
let command_range_end = Point::new(
position.row,
call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
);
let command_range = buffer.anchor_after(command_range_start)
..buffer.anchor_after(command_range_end);
let name = line[call.name.clone()].to_string();
Some(if let Some(argument) = call.argument {
let start =
buffer.anchor_after(Point::new(position.row, argument.start as u32));
let argument = line[argument.clone()].to_string();
(name, Some(argument), command_range, start..buffer_position)
} else {
let start =
buffer.anchor_after(Point::new(position.row, call.name.start as u32));
(name, None, command_range, start..buffer_position)
})
})
else {
return Task::ready(Ok(Vec::new()));
};
if let Some(argument) = argument {
self.complete_command_argument(&name, argument, command_range, argument_range, cx)
} else {
self.complete_command_name(&name, command_range, argument_range, cx)
}
}
fn resolve_completions(
&self,
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
SlashCommandLine::parse(line).is_some()
} else {
false
}
}
}
impl SlashCommandLine {
pub(crate) fn parse(line: &str) -> Option<Self> {
let mut call: Option<Self> = None;
let mut ix = 0;
for c in line.chars() {
let next_ix = ix + c.len_utf8();
if let Some(call) = &mut call {
// The command arguments start at the first non-whitespace character
// after the command name, and continue until the end of the line.
if let Some(argument) = &mut call.argument {
if (*argument).is_empty() && c.is_whitespace() {
argument.start = next_ix;
}
argument.end = next_ix;
}
// The command name ends at the first whitespace character.
else if !call.name.is_empty() {
if c.is_whitespace() {
call.argument = Some(next_ix..next_ix);
} else {
call.name.end = next_ix;
}
}
// The command name must begin with a letter.
else if c.is_alphabetic() {
call.name.end = next_ix;
} else {
return None;
}
}
// Commands start with a slash.
else if c == '/' {
call = Some(SlashCommandLine {
name: next_ix..next_ix,
argument: None,
});
}
// The line can't contain anything before the slash except for whitespace.
else if !c.is_whitespace() {
return None;
}
ix = next_ix;
}
call
}
}

View File

@@ -1,105 +0,0 @@
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use editor::Editor;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::{borrow::Cow, sync::Arc};
use ui::{IntoElement, WindowContext};
use workspace::Workspace;
pub(crate) struct ActiveSlashCommand;
impl SlashCommand for ActiveSlashCommand {
fn name(&self) -> String {
"active".into()
}
fn description(&self) -> String {
"insert active tab".into()
}
fn menu_text(&self) -> String {
"Insert Active Tab".into()
}
fn complete_argument(
&self,
_query: String,
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let output = workspace.update(cx, |workspace, cx| {
let Some(active_item) = workspace.active_item(cx) else {
return Task::ready(Err(anyhow!("no active tab")));
};
let Some(buffer) = active_item
.downcast::<Editor>()
.and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
else {
return Task::ready(Err(anyhow!("active tab is not an editor")));
};
let snapshot = buffer.read(cx).snapshot();
let path = snapshot.resolve_file_path(cx, true);
let text = cx.background_executor().spawn({
let path = path.clone();
async move {
let path = path
.as_ref()
.map(|path| path.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed("untitled"));
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
output.push_str("```");
output.push_str(&path);
output.push('\n');
for chunk in snapshot.as_rope().chunks() {
output.push_str(chunk);
}
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
output
}
});
cx.foreground_executor().spawn(async move {
let text = text.await;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _| {
FilePlaceholder {
id,
path: path.clone(),
line_range: None,
unfold,
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -1,81 +0,0 @@
use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::{
fmt::Write,
sync::{atomic::AtomicBool, Arc},
};
use ui::prelude::*;
use workspace::Workspace;
pub(crate) struct DefaultSlashCommand;
impl SlashCommand for DefaultSlashCommand {
fn name(&self) -> String {
"default".into()
}
fn description(&self) -> String {
"insert default prompt".into()
}
fn menu_text(&self) -> String {
"Insert Default Prompt".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
_query: String,
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {
let store = store.await?;
let prompts = store.default_prompt_metadata();
let mut text = String::new();
writeln!(text, "Default Prompt:").unwrap();
for prompt in prompts {
if let Some(title) = prompt.title {
writeln!(text, "/prompt {}", title).unwrap();
}
}
text.pop();
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..text.len(),
render_placeholder: Arc::new(move |id, unfold, _cx| {
PromptPlaceholder {
title: "Default".into(),
id,
unfold,
}
.into_any_element()
}),
}],
text,
run_commands_in_text: true,
})
})
}
}

View File

@@ -1,444 +0,0 @@
use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fuzzy::{PathMatch, StringMatchCandidate};
use gpui::{svg, AppContext, Model, RenderOnce, Task, View, WeakView};
use language::{
Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate,
OffsetRangeExt, ToOffset,
};
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
use rope::Point;
use std::fmt::Write;
use std::{
ops::Range,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use util::paths::PathMatcher;
use util::ResultExt;
use workspace::Workspace;
pub(crate) struct DiagnosticsCommand;
impl DiagnosticsCommand {
fn search_paths(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let path_prefix: Arc<str> = "".into();
Task::ready(
entries
.into_iter()
.map(|(entry, _)| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
})
.collect(),
)
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: false,
candidates: project::Candidates::Entries,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl SlashCommand for DiagnosticsCommand {
fn name(&self) -> String {
"diagnostics".into()
}
fn description(&self) -> String {
"Insert diagnostics".into()
}
fn menu_text(&self) -> String {
"Insert Diagnostics".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let query = query.split_whitespace().last().unwrap_or("").to_string();
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
let mut matches: Vec<String> = paths
.await
.into_iter()
.map(|path_match| {
format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
})
.collect();
matches.extend(
fuzzy::match_strings(
&Options::match_candidates_for_args(),
&query,
false,
10,
&cancellation_flag,
executor,
)
.await
.into_iter()
.map(|candidate| candidate.string),
);
Ok(matches)
})
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let options = Options::parse(argument);
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
cx.spawn(move |_| async move {
let (text, sections) = task.await?;
Ok(SlashCommandOutput {
text,
sections: sections
.into_iter()
.map(|(range, placeholder_type)| SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
DiagnosticsPlaceholder {
id,
unfold,
placeholder_type: placeholder_type.clone(),
}
.into_any_element()
}),
})
.collect(),
run_commands_in_text: false,
})
})
}
}
#[derive(Default)]
struct Options {
include_warnings: bool,
path_matcher: Option<PathMatcher>,
}
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
impl Options {
pub fn parse(arguments_line: Option<&str>) -> Self {
arguments_line
.map(|arguments_line| {
let args = arguments_line.split_whitespace().collect::<Vec<_>>();
let mut include_warnings = false;
let mut path_matcher = None;
for arg in args {
if arg == INCLUDE_WARNINGS_ARGUMENT {
include_warnings = true;
} else {
path_matcher = PathMatcher::new(arg).log_err();
}
}
Self {
include_warnings,
path_matcher,
}
})
.unwrap_or_default()
}
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
[StringMatchCandidate::new(
0,
INCLUDE_WARNINGS_ARGUMENT.to_string(),
)]
}
}
fn collect_diagnostics(
project: Model<Project>,
options: Options,
cx: &mut AppContext,
) -> Task<Result<(String, Vec<(Range<usize>, PlaceholderType)>)>> {
let header = if let Some(path_matcher) = &options.path_matcher {
format!("diagnostics: {}", path_matcher.source())
} else {
"diagnostics".to_string()
};
let project_handle = project.downgrade();
let diagnostic_summaries: Vec<_> = project.read(cx).diagnostic_summaries(false, cx).collect();
cx.spawn(|mut cx| async move {
let mut text = String::new();
writeln!(text, "{}", &header).unwrap();
let mut sections: Vec<(Range<usize>, PlaceholderType)> = Vec::new();
let mut project_summary = DiagnosticSummary::default();
for (project_path, _, summary) in diagnostic_summaries {
if let Some(path_matcher) = &options.path_matcher {
if !path_matcher.is_match(&project_path.path) {
continue;
}
}
project_summary.error_count += summary.error_count;
if options.include_warnings {
project_summary.warning_count += summary.warning_count;
} else if summary.error_count == 0 {
continue;
}
let last_end = text.len();
let file_path = project_path.path.to_string_lossy().to_string();
writeln!(&mut text, "{file_path}").unwrap();
if let Some(buffer) = project_handle
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))?
.await
.log_err()
{
collect_buffer_diagnostics(
&mut text,
&mut sections,
cx.read_model(&buffer, |buffer, _| buffer.snapshot())?,
options.include_warnings,
);
}
sections.push((
last_end..text.len().saturating_sub(1),
PlaceholderType::File(file_path),
))
}
sections.push((
0..text.len(),
PlaceholderType::Root(project_summary, header),
));
Ok((text, sections))
})
}
fn collect_buffer_diagnostics(
text: &mut String,
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
snapshot: BufferSnapshot,
include_warnings: bool,
) {
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
collect_diagnostic(text, sections, entry, &snapshot, include_warnings)
}
}
fn collect_diagnostic(
text: &mut String,
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
entry: &DiagnosticEntry<Anchor>,
snapshot: &BufferSnapshot,
include_warnings: bool,
) {
const EXCERPT_EXPANSION_SIZE: u32 = 2;
const MAX_MESSAGE_LENGTH: usize = 2000;
let ty = match entry.diagnostic.severity {
DiagnosticSeverity::WARNING => {
if !include_warnings {
return;
}
DiagnosticType::Warning
}
DiagnosticSeverity::ERROR => DiagnosticType::Error,
_ => return,
};
let prev_len = text.len();
let range = entry.range.to_point(snapshot);
let diagnostic_row_number = range.start.row + 1;
let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
let excerpt_range =
Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot);
text.push_str("```");
if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
text.push_str(&language_name);
}
text.push('\n');
let mut buffer_text = String::new();
for chunk in snapshot.text_for_range(excerpt_range) {
buffer_text.push_str(chunk);
}
for (i, line) in buffer_text.lines().enumerate() {
let line_number = start_row + i as u32 + 1;
writeln!(text, "{}", line).unwrap();
if line_number == diagnostic_row_number {
text.push_str("//");
let prev_len = text.len();
write!(text, " {}: ", ty.as_str()).unwrap();
let padding = text.len() - prev_len;
let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
.replace('\n', format!("\n//{:padding$}", "").as_str());
writeln!(text, "{message}").unwrap();
}
}
writeln!(text, "```").unwrap();
sections.push((
prev_len..text.len().saturating_sub(1),
PlaceholderType::Diagnostic(ty, entry.diagnostic.message.clone()),
))
}
#[derive(Clone)]
pub enum PlaceholderType {
Root(DiagnosticSummary, String),
File(String),
Diagnostic(DiagnosticType, String),
}
#[derive(Copy, Clone, IntoElement)]
pub enum DiagnosticType {
Warning,
Error,
}
impl DiagnosticType {
pub fn as_str(&self) -> &'static str {
match self {
DiagnosticType::Warning => "warning",
DiagnosticType::Error => "error",
}
}
}
#[derive(IntoElement)]
pub struct DiagnosticsPlaceholder {
pub id: ElementId,
pub placeholder_type: PlaceholderType,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
}
impl RenderOnce for DiagnosticsPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
let (icon, content) = match self.placeholder_type {
PlaceholderType::Root(summary, title) => (
h_flex()
.w_full()
.gap_0p5()
.when(summary.error_count > 0, |this| {
this.child(DiagnosticType::Error)
.child(Label::new(summary.error_count.to_string()))
})
.when(summary.warning_count > 0, |this| {
this.child(DiagnosticType::Warning)
.child(Label::new(summary.warning_count.to_string()))
})
.into_any_element(),
Label::new(title),
),
PlaceholderType::File(file) => (
Icon::new(IconName::File).into_any_element(),
Label::new(file),
),
PlaceholderType::Diagnostic(diagnostic_type, message) => (
diagnostic_type.into_any_element(),
Label::new(message).single_line(),
),
};
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(icon)
.child(content)
.on_click(move |_, cx| unfold(cx))
}
}
impl RenderOnce for DiagnosticType {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
svg()
.size(cx.text_style().font_size)
.flex_none()
.map(|icon| match self {
DiagnosticType::Error => icon
.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx)),
DiagnosticType::Warning => icon
.path(IconName::ExclamationTriangle.path())
.text_color(Color::Warning.color(cx)),
})
}
}

View File

@@ -1,188 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub(crate) struct FetchSlashCommand;
impl FetchSlashCommand {
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") {
url = format!("https://{url}");
}
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
}
impl SlashCommand for FetchSlashCommand {
fn name(&self) -> String {
"fetch".into()
}
fn description(&self) -> String {
"insert URL contents".into()
}
fn menu_text(&self) -> String {
"Insert fetched URL contents".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing URL")));
};
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let http_client = workspace.read(cx).client().http_client();
let url = argument.to_string();
let text = cx.background_executor().spawn({
let url = url.clone();
async move { Self::build_message(http_client, &url).await }
});
let url = SharedString::from(url);
cx.foreground_executor().spawn(async move {
let text = text.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
FetchPlaceholder {
id,
unfold,
url: url.clone(),
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}
}
#[derive(IntoElement)]
struct FetchPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub url: SharedString,
}
impl RenderOnce for FetchPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::AtSign))
.child(Label::new(format!("fetch {url}", url = self.url)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -1,226 +0,0 @@
use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fuzzy::PathMatch;
use gpui::{AppContext, RenderOnce, SharedString, Task, View, WeakView};
use language::{LineEnding, LspAdapterDelegate};
use project::PathMatchCandidateSet;
use std::{
ops::Range,
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct FileSlashCommand;
impl FileSlashCommand {
fn search_paths(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
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();
Task::ready(
entries
.into_iter()
.filter_map(|(entry, _)| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: full_path.into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
})
})
.collect(),
)
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Files,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl SlashCommand for FileSlashCommand {
fn name(&self) -> String {
"file".into()
}
fn description(&self) -> String {
"insert file".into()
}
fn menu_text(&self) -> String {
"Insert File".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
cx.background_executor().spawn(async move {
Ok(paths
.await
.into_iter()
.map(|path_match| {
format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
})
.collect())
})
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing path")));
};
let path = PathBuf::from(argument);
let abs_path = workspace
.read(cx)
.visible_worktrees(cx)
.find_map(|worktree| {
let worktree = worktree.read(cx);
let worktree_root_path = Path::new(worktree.root_name());
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
worktree.absolutize(&relative_path).ok()
});
let Some(abs_path) = abs_path else {
return Task::ready(Err(anyhow!("missing path")));
};
let fs = workspace.read(cx).app_state().fs.clone();
let argument = argument.to_string();
let text = cx.background_executor().spawn(async move {
let mut content = fs.load(&abs_path).await?;
LineEnding::normalize(&mut content);
let mut output = String::with_capacity(argument.len() + content.len() + 9);
output.push_str("```");
output.push_str(&argument);
output.push('\n');
output.push_str(&content);
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
anyhow::Ok(output)
});
cx.foreground_executor().spawn(async move {
let text = text.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
FilePlaceholder {
path: Some(path.clone()),
line_range: None,
id,
unfold,
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}
}
#[derive(IntoElement)]
pub struct FilePlaceholder {
pub path: Option<PathBuf>,
pub line_range: Option<Range<u32>>,
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
}
impl RenderOnce for FilePlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
let title = if let Some(path) = self.path.as_ref() {
SharedString::from(path.to_string_lossy().to_string())
} else {
SharedString::from("untitled")
};
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::File))
.child(Label::new(title))
.when_some(self.line_range, |button, line_range| {
button.child(Label::new(":")).child(Label::new(format!(
"{}-{}",
line_range.start, line_range.end
)))
})
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -1,83 +0,0 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use chrono::{DateTime, Local};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct NowSlashCommand;
impl SlashCommand for NowSlashCommand {
fn name(&self) -> String {
"now".into()
}
fn description(&self) -> String {
"insert the current date and time".into()
}
fn menu_text(&self) -> String {
"Insert current date and time".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
_cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let now = Local::now();
let text = format!("Today is {now}.", now = now.to_rfc3339());
let range = 0..text.len();
Task::ready(Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
NowPlaceholder { id, unfold, now }.into_any_element()
}),
}],
run_commands_in_text: false,
}))
}
}
#[derive(IntoElement)]
struct NowPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub now: DateTime<Local>,
}
impl RenderOnce for NowPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::CountdownTimer))
.child(Label::new(self.now.to_rfc3339()))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -1,157 +0,0 @@
use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Context, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fs::Fs;
use gpui::{AppContext, Model, Task, WeakView};
use language::LspAdapterDelegate;
use project::{Project, ProjectPath};
use std::{
fmt::Write,
path::Path,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct ProjectSlashCommand;
impl ProjectSlashCommand {
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
let buffer = fs.load(path_to_cargo_toml).await?;
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
let mut message = String::new();
writeln!(message, "You are in a Rust project.")?;
if let Some(workspace) = cargo_toml.workspace {
writeln!(
message,
"The project is a Cargo workspace with the following members:"
)?;
for member in workspace.members {
writeln!(message, "- {member}")?;
}
if !workspace.default_members.is_empty() {
writeln!(message, "The default members are:")?;
for member in workspace.default_members {
writeln!(message, "- {member}")?;
}
}
if !workspace.dependencies.is_empty() {
writeln!(
message,
"The following workspace dependencies are installed:"
)?;
for dependency in workspace.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
} else if let Some(package) = cargo_toml.package {
writeln!(
message,
"The project name is \"{name}\".",
name = package.name
)?;
let description = package
.description
.as_ref()
.and_then(|description| description.get().ok().cloned());
if let Some(description) = description.as_ref() {
writeln!(message, "It describes itself as \"{description}\".")?;
}
if !cargo_toml.dependencies.is_empty() {
writeln!(message, "The following dependencies are installed:")?;
for dependency in cargo_toml.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
}
Ok(message)
}
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
let worktree = project.read(cx).worktrees().next()?;
let worktree = worktree.read(cx);
let entry = worktree.entry_for_path("Cargo.toml")?;
let path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
Some(Arc::from(
project.read(cx).absolute_path(&path, cx)?.as_path(),
))
}
}
impl SlashCommand for ProjectSlashCommand {
fn name(&self) -> String {
"project".into()
}
fn description(&self) -> String {
"insert project metadata".into()
}
fn menu_text(&self) -> String {
"Insert Project Metadata".into()
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let output = workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
let fs = workspace.project().read(cx).fs().clone();
let path = Self::path_to_cargo_toml(project, cx);
let output = cx.background_executor().spawn(async move {
let path = path.with_context(|| "Cargo.toml not found")?;
Self::build_message(fs, &path).await
});
cx.foreground_executor().spawn(async move {
let text = output.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
ButtonLike::new(id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::FileTree))
.child(Label::new("Project"))
.on_click(move |_, cx| unfold(cx))
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -1,110 +0,0 @@
use super::{SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Context, Result};
use assistant_slash_command::SlashCommandOutputSection;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct PromptSlashCommand;
impl SlashCommand for PromptSlashCommand {
fn name(&self) -> String {
"prompt".into()
}
fn description(&self) -> String {
"insert prompt from library".into()
}
fn menu_text(&self) -> String {
"Insert Prompt from Library".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {
let prompts = store.await?.search(query).await;
Ok(prompts
.into_iter()
.filter_map(|prompt| Some(prompt.title?.to_string()))
.collect())
})
}
fn run(
self: Arc<Self>,
title: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(title) = title else {
return Task::ready(Err(anyhow!("missing prompt name")));
};
let store = PromptStore::global(cx);
let title = SharedString::from(title.to_string());
let prompt = cx.background_executor().spawn({
let title = title.clone();
async move {
let store = store.await?;
let prompt_id = store
.id_for_title(&title)
.with_context(|| format!("no prompt found with title {:?}", title))?;
let body = store.load(prompt_id).await?;
anyhow::Ok(body)
}
});
cx.foreground_executor().spawn(async move {
let prompt = prompt.await?;
let range = 0..prompt.len();
Ok(SlashCommandOutput {
text: prompt,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
PromptPlaceholder {
id,
unfold,
title: title.clone(),
}
.into_any_element()
}),
}],
run_commands_in_text: true,
})
})
}
}
#[derive(IntoElement)]
pub struct PromptPlaceholder {
pub title: SharedString,
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
}
impl RenderOnce for PromptPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::Library))
.child(Label::new(self.title))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -1,270 +0,0 @@
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use fs::Fs;
use futures::AsyncReadExt;
use gpui::{AppContext, Model, Task, WeakView};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use project::{Project, ProjectPath};
use rustdoc::{convert_rustdoc_to_markdown, RustdocStore};
use rustdoc::{CrateName, LocalProvider};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use util::{maybe, ResultExt};
use workspace::Workspace;
#[derive(Debug, Clone, Copy)]
enum RustdocSource {
/// The docs were sourced from local `cargo doc` output.
Local,
/// The docs were sourced from `docs.rs`.
DocsDotRs,
}
pub(crate) struct RustdocSlashCommand;
impl RustdocSlashCommand {
async fn build_message(
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
crate_name: CrateName,
module_path: Vec<String>,
path_to_cargo_toml: Option<&Path>,
) -> Result<(RustdocSource, String)> {
let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent());
if let Some(cargo_workspace_root) = cargo_workspace_root {
let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc");
local_cargo_doc_path.push(crate_name.as_ref());
if !module_path.is_empty() {
local_cargo_doc_path.push(module_path.join("/"));
}
local_cargo_doc_path.push("index.html");
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
let (markdown, _items) = convert_rustdoc_to_markdown(contents.as_bytes())?;
return Ok((RustdocSource::Local, markdown));
}
}
let version = "latest";
let path = format!(
"{crate_name}/{version}/{crate_name}/{module_path}",
module_path = module_path.join("/")
);
let mut response = http_client
.get(
&format!("https://docs.rs/{path}"),
AsyncBody::default(),
true,
)
.await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading docs.rs response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let (markdown, _items) = convert_rustdoc_to_markdown(&body[..])?;
Ok((RustdocSource::DocsDotRs, markdown))
}
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
let worktree = project.read(cx).worktrees().next()?;
let worktree = worktree.read(cx);
let entry = worktree.entry_for_path("Cargo.toml")?;
let path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
Some(Arc::from(
project.read(cx).absolute_path(&path, cx)?.as_path(),
))
}
}
impl SlashCommand for RustdocSlashCommand {
fn name(&self) -> String {
"rustdoc".into()
}
fn description(&self) -> String {
"insert Rust docs".into()
}
fn menu_text(&self) -> String {
"Insert Rust Documentation".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
_cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let index_provider_deps = maybe!({
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
anyhow::Ok((fs, cargo_workspace_root))
});
let store = RustdocStore::global(cx);
cx.background_executor().spawn(async move {
if let Some((crate_name, rest)) = query.split_once(':') {
if rest.is_empty() {
if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
let provider = Box::new(LocalProvider::new(fs, cargo_workspace_root));
// We don't need to hold onto this task, as the `RustdocStore` will hold it
// until it completes.
let _ = store.clone().index(crate_name.into(), provider);
}
}
}
let items = store.search(query).await;
Ok(items)
})
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing crate name")));
};
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let http_client = workspace.read(cx).client().http_client();
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
let mut path_components = argument.split("::");
let crate_name = match path_components
.next()
.ok_or_else(|| anyhow!("missing crate name"))
{
Ok(crate_name) => CrateName::from(crate_name),
Err(err) => return Task::ready(Err(err)),
};
let item_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
let text = cx.background_executor().spawn({
let rustdoc_store = RustdocStore::global(cx);
let crate_name = crate_name.clone();
let item_path = item_path.clone();
async move {
let item_docs = rustdoc_store
.load(crate_name.clone(), Some(item_path.join("::")))
.await;
if let Ok(item_docs) = item_docs {
anyhow::Ok((RustdocSource::Local, item_docs.docs().to_owned()))
} else {
Self::build_message(
fs,
http_client,
crate_name,
item_path,
path_to_cargo_toml.as_deref(),
)
.await
}
}
});
let module_path = if item_path.is_empty() {
None
} else {
Some(SharedString::from(item_path.join("::")))
};
cx.foreground_executor().spawn(async move {
let (source, text) = text.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
RustdocPlaceholder {
id,
unfold,
source,
crate_name: crate_name.clone(),
module_path: module_path.clone(),
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}
}
#[derive(IntoElement)]
struct RustdocPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub source: RustdocSource,
pub crate_name: CrateName,
pub module_path: Option<SharedString>,
}
impl RenderOnce for RustdocPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
let crate_path = self
.module_path
.map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
.unwrap_or(self.crate_name.to_string());
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::FileRust))
.child(Label::new(format!(
"rustdoc ({source}): {crate_path}",
source = match self.source {
RustdocSource::Local => "local",
RustdocSource::DocsDotRs => "docs.rs",
}
)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -1,195 +0,0 @@
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
use anyhow::Result;
use assistant_slash_command::SlashCommandOutputSection;
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
use semantic_index::SemanticIndex;
use std::{
fmt::Write,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ButtonLike, ElevationIndex, Icon, IconName};
use util::ResultExt;
use workspace::Workspace;
pub(crate) struct SearchSlashCommand;
impl SlashCommand for SearchSlashCommand {
fn name(&self) -> String {
"search".into()
}
fn label(&self, cx: &AppContext) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str("search ", None);
label.push_str(
"--n",
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0.."search".len();
label
}
fn description(&self) -> String {
"semantic search".into()
}
fn menu_text(&self) -> String {
"Semantic Search".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
let Some(argument) = argument else {
return Task::ready(Err(anyhow::anyhow!("missing search query")));
};
let mut limit = None;
let mut query = String::new();
for part in argument.split(' ') {
if let Some(parameter) = part.strip_prefix("--") {
if let Ok(count) = parameter.parse::<usize>() {
limit = Some(count);
continue;
}
}
query.push_str(part);
query.push(' ');
}
query.pop();
if query.is_empty() {
return Task::ready(Err(anyhow::anyhow!("missing search query")));
}
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let project_index =
cx.update_global(|index: &mut SemanticIndex, cx| index.project_index(project, cx));
cx.spawn(|cx| async move {
let results = project_index
.read_with(&cx, |project_index, cx| {
project_index.search(query.clone(), limit.unwrap_or(5), cx)
})?
.await?;
let mut loaded_results = Vec::new();
for result in results {
let (full_path, file_content) =
result.worktree.read_with(&cx, |worktree, _cx| {
let entry_abs_path = worktree.abs_path().join(&result.path);
let mut entry_full_path = PathBuf::from(worktree.root_name());
entry_full_path.push(&result.path);
let file_content = async {
let entry_abs_path = entry_abs_path;
fs.load(&entry_abs_path).await
};
(entry_full_path, file_content)
})?;
if let Some(file_content) = file_content.await.log_err() {
loaded_results.push((result, full_path, file_content));
}
}
let output = cx
.background_executor()
.spawn(async move {
let mut text = format!("Search results for {query}:\n");
let mut sections = Vec::new();
for (result, full_path, file_content) in loaded_results {
let range_start = result.range.start.min(file_content.len());
let range_end = result.range.end.min(file_content.len());
let start_line =
file_content[0..range_start].matches('\n').count() as u32 + 1;
let end_line = file_content[0..range_end].matches('\n').count() as u32 + 1;
let start_line_byte_offset = file_content[0..range_start]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or_default();
let end_line_byte_offset = file_content[range_end..]
.find('\n')
.map(|pos| range_end + pos)
.unwrap_or_else(|| file_content.len());
let section_start_ix = text.len();
writeln!(
text,
"```{}:{}-{}",
result.path.display(),
start_line,
end_line,
)
.unwrap();
let mut excerpt =
file_content[start_line_byte_offset..end_line_byte_offset].to_string();
LineEnding::normalize(&mut excerpt);
text.push_str(&excerpt);
writeln!(text, "\n```\n").unwrap();
let section_end_ix = text.len() - 1;
sections.push(SlashCommandOutputSection {
range: section_start_ix..section_end_ix,
render_placeholder: Arc::new(move |id, unfold, _| {
FilePlaceholder {
id,
path: Some(full_path.clone()),
line_range: Some(start_line..end_line),
unfold,
}
.into_any_element()
}),
});
}
let query = SharedString::from(query);
sections.push(SlashCommandOutputSection {
range: 0..text.len(),
render_placeholder: Arc::new(move |id, unfold, _cx| {
ButtonLike::new(id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(query.clone()))
.on_click(move |_, cx| unfold(cx))
.into_any_element()
}),
});
SlashCommandOutput {
text,
sections,
run_commands_in_text: false,
}
})
.await;
Ok(output)
})
}
}

View File

@@ -1,121 +0,0 @@
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use collections::HashMap;
use editor::Editor;
use gpui::{AppContext, Entity, Task, WeakView};
use language::LspAdapterDelegate;
use std::{fmt::Write, path::Path, sync::Arc};
use ui::{IntoElement, WindowContext};
use workspace::Workspace;
pub(crate) struct TabsSlashCommand;
impl SlashCommand for TabsSlashCommand {
fn name(&self) -> String {
"tabs".into()
}
fn description(&self) -> String {
"insert open tabs".into()
}
fn menu_text(&self) -> String {
"Insert Open Tabs".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<std::sync::atomic::AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let open_buffers = workspace.update(cx, |workspace, cx| {
let mut timestamps_by_entity_id = HashMap::default();
let mut open_buffers = Vec::new();
for pane in workspace.panes() {
let pane = pane.read(cx);
for entry in pane.activation_history() {
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
}
}
for editor in workspace.items_of_type::<Editor>(cx) {
if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
if let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) {
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
open_buffers.push((full_path, snapshot, *timestamp));
}
}
}
open_buffers
});
match open_buffers {
Ok(mut open_buffers) => cx.background_executor().spawn(async move {
open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
let mut sections = Vec::new();
let mut text = String::new();
for (full_path, buffer, _) in open_buffers {
let section_start_ix = text.len();
writeln!(
text,
"```{}\n",
full_path
.as_deref()
.unwrap_or(Path::new("untitled"))
.display()
)
.unwrap();
for chunk in buffer.as_rope().chunks() {
text.push_str(chunk);
}
if !text.ends_with('\n') {
text.push('\n');
}
writeln!(text, "```\n").unwrap();
let section_end_ix = text.len() - 1;
sections.push(SlashCommandOutputSection {
range: section_start_ix..section_end_ix,
render_placeholder: Arc::new(move |id, unfold, _| {
FilePlaceholder {
id,
path: full_path.clone(),
line_range: None,
unfold,
}
.into_any_element()
}),
});
}
Ok(SlashCommandOutput {
text,
sections,
run_commands_in_text: false,
})
}),
Err(error) => Task::ready(Err(error)),
}
}
}

View File

@@ -1,86 +0,0 @@
When the user asks you to suggest edits for a buffer, use a strict template consisting of:
* A markdown code block with the file path as the language identifier.
* The original code that should be replaced
* A separator line (`---`)
* The new text that should replace the original lines
Each code block may only contain an edit for one single contiguous range of text. Use multiple code blocks for multiple edits.
## Example
If you have a buffer with the following lines:
```path/to/file.rs
fn quicksort(arr: &mut [i32]) {
if arr.len() <= 1 {
return;
}
let pivot_index = partition(arr);
let (left, right) = arr.split_at_mut(pivot_index);
quicksort(left);
quicksort(&mut right[1..]);
}
fn partition(arr: &mut [i32]) -> usize {
let last_index = arr.len() - 1;
let pivot = arr[last_index];
let mut i = 0;
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, last_index);
i
}
```
And you want to replace the for loop inside `partition`, output the following.
```edit path/to/file.rs
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
---
let mut j = 0;
while j < last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
j += 1;
}
```
If you wanted to insert comments above the partition function, output the following:
```edit path/to/file.rs
fn partition(arr: &mut [i32]) -> usize {
---
// A helper function used for quicksort.
fn partition(arr: &mut [i32]) -> usize {
```
If you wanted to delete the partition function, output the following:
```edit path/to/file.rs
fn partition(arr: &mut [i32]) -> usize {
let last_index = arr.len() - 1;
let pivot = arr[last_index];
let mut i = 0;
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, last_index);
i
}
---
```

View File

@@ -0,0 +1,58 @@
[package]
name = "assistant2"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant2.rs"
[features]
default = []
stories = ["dep:story"]
[dependencies]
anyhow.workspace = true
assistant_tooling.workspace = true
client.workspace = true
collections.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
project.workspace = true
rich_text.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]
assets.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
languages.workspace = true
node_runtime.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
release_channel.workspace = true
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1 @@
> Give me a comprehensive list of all the elements define in my project (impl Element for {}, impl<T: 'static> Element for {}, impl IntoElement for {})

View File

@@ -0,0 +1 @@
> What are all the places we define a new gpui element in my project? (impl Element for {})

View File

@@ -0,0 +1 @@
> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less.

View File

@@ -0,0 +1,911 @@
mod assistant_settings;
mod attachments;
mod completion_provider;
mod tools;
pub mod ui;
use crate::{
attachments::ActiveEditorAttachmentTool,
tools::{CreateBufferTool, ProjectIndexTool},
ui::UserOrAssistant,
};
use ::ui::{div, prelude::*, Color, ViewContext};
use anyhow::{Context, Result};
use assistant_tooling::{
AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry, UserAttachment,
};
use client::{proto, Client, UserStore};
use collections::HashMap;
use completion_provider::*;
use editor::Editor;
use feature_flags::FeatureFlagAppExt as _;
use futures::{future::join_all, StreamExt};
use gpui::{
list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
};
use language::{language_settings::SoftWrap, LanguageRegistry};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
use rich_text::RichText;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
use serde::Deserialize;
use settings::Settings;
use std::sync::Arc;
use ui::{ActiveFileButton, Composer, ProjectIndexButton};
use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Workspace,
};
pub use assistant_settings::AssistantSettings;
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
pub struct Submit(SubmitMode);
/// There are multiple different ways to submit a model request, represented by this enum.
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
pub enum SubmitMode {
/// Only include the conversation.
Simple,
/// Send the current file as context.
CurrentFile,
/// Search the codebase and send relevant excerpts.
Codebase,
}
gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex]);
gpui::impl_actions!(assistant2, [Submit]);
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
AssistantSettings::register(cx);
cx.spawn(|mut cx| {
let client = client.clone();
async move {
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticIndex::new(
EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
cx.set_global(CompletionProvider::new(CloudCompletionProvider::new(
client,
)));
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
workspace.register_action(|workspace, _: &DebugProjectIndex, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
let index = panel.read(cx).chat.read(cx).project_index.clone();
let view = cx.new_view(|cx| ProjectIndexDebugView::new(index, cx));
workspace.add_item_to_center(Box::new(view), cx);
}
});
},
)
.detach();
}
pub fn enabled(cx: &AppContext) -> bool {
cx.is_staff()
}
pub struct AssistantPanel {
chat: View<AssistantChat>,
width: Option<Pixels>,
}
impl AssistantPanel {
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let (app_state, project) = workspace.update(&mut cx, |workspace, _| {
(workspace.app_state().clone(), workspace.project().clone())
})?;
cx.new_view(|cx| {
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
semantic_index.project_index(project.clone(), cx)
});
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(ProjectIndexTool::new(project_index.clone()), cx)
.context("failed to register ProjectIndexTool")
.log_err();
tool_registry
.register(
CreateBufferTool::new(workspace.clone(), project.clone()),
cx,
)
.context("failed to register CreateBufferTool")
.log_err();
let mut attachment_store = AttachmentRegistry::new();
attachment_store.register(ActiveEditorAttachmentTool::new(workspace.clone(), cx));
Self::new(
app_state.languages.clone(),
Arc::new(tool_registry),
Arc::new(attachment_store),
app_state.user_store.clone(),
project_index,
workspace,
cx,
)
})
})
}
pub fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
attachment_store: Arc<AttachmentRegistry>,
user_store: Model<UserStore>,
project_index: Model<ProjectIndex>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let chat = cx.new_view(|cx| {
AssistantChat::new(
language_registry,
tool_registry.clone(),
attachment_store,
user_store,
project_index,
workspace,
cx,
)
});
Self { width: None, chat }
}
}
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.size_full()
.v_flex()
.bg(cx.theme().colors().panel_background)
.child(self.chat.clone())
}
}
impl Panel for AssistantPanel {
fn persistent_name() -> &'static str {
"AssistantPanelv2"
}
fn position(&self, _cx: &WindowContext) -> workspace::dock::DockPosition {
// todo!("Add a setting / use assistant settings")
DockPosition::Right
}
fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool {
matches!(position, DockPosition::Right)
}
fn set_position(&mut self, _: workspace::dock::DockPosition, _: &mut ViewContext<Self>) {
// Do nothing until we have a setting for this
}
fn size(&self, _cx: &WindowContext) -> Pixels {
self.width.unwrap_or(px(400.))
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
self.width = size;
cx.notify();
}
fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> {
Some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
Some("Assistant Panel ✨")
}
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
}
impl EventEmitter<PanelEvent> for AssistantPanel {}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.chat.read(cx).composer_editor.read(cx).focus_handle(cx)
}
}
pub struct AssistantChat {
model: String,
messages: Vec<ChatMessage>,
list_state: ListState,
language_registry: Arc<LanguageRegistry>,
composer_editor: View<Editor>,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
user_store: Model<UserStore>,
next_message_id: MessageId,
collapsed_messages: HashMap<MessageId, bool>,
editing_message: Option<EditingMessage>,
pending_completion: Option<Task<()>>,
tool_registry: Arc<ToolRegistry>,
attachment_registry: Arc<AttachmentRegistry>,
project_index: Model<ProjectIndex>,
}
struct EditingMessage {
id: MessageId,
old_body: Arc<str>,
body: View<Editor>,
}
impl AssistantChat {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
attachment_registry: Arc<AttachmentRegistry>,
user_store: Model<UserStore>,
project_index: Model<ProjectIndex>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let model = CompletionProvider::get(cx).default_model();
let view = cx.view().downgrade();
let list_state = ListState::new(
0,
ListAlignment::Bottom,
px(1024.),
move |ix, cx: &mut WindowContext| {
view.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
},
);
let project_index_button = cx.new_view(|cx| {
ProjectIndexButton::new(project_index.clone(), tool_registry.clone(), cx)
});
let active_file_button = match workspace.upgrade() {
Some(workspace) => {
Some(cx.new_view(
|cx| ActiveFileButton::new(attachment_registry.clone(), workspace, cx), //
))
}
_ => None,
};
Self {
model,
messages: Vec::new(),
composer_editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Send a message…", cx);
editor
}),
list_state,
user_store,
language_registry,
project_index_button,
active_file_button,
project_index,
next_message_id: MessageId(0),
editing_message: None,
collapsed_messages: HashMap::default(),
pending_completion: None,
attachment_registry,
tool_registry,
}
}
fn editing_message_id(&self) -> Option<MessageId> {
self.editing_message.as_ref().map(|message| message.id)
}
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
self.messages.iter().find_map(|message| match message {
ChatMessage::User(message) => message
.body
.focus_handle(cx)
.contains_focused(cx)
.then_some(message.id),
ChatMessage::Assistant(_) => None,
})
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
// If we're currently editing a message, cancel the edit.
if let Some(editing_message) = self.editing_message.take() {
editing_message
.body
.update(cx, |body, cx| body.set_text(editing_message.old_body, cx));
return;
}
if self.pending_completion.take().is_some() {
if let Some(ChatMessage::Assistant(message)) = self.messages.last() {
if message.body.text.is_empty() {
self.pop_message(cx);
}
}
return;
}
cx.propagate();
}
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
if let Some(focused_message_id) = self.focused_message_id(cx) {
self.truncate_messages(focused_message_id, cx);
self.pending_completion.take();
self.composer_editor.focus_handle(cx).focus(cx);
if self.editing_message_id() == Some(focused_message_id) {
self.editing_message.take();
}
} else if self.composer_editor.focus_handle(cx).is_focused(cx) {
// Don't allow multiple concurrent completions.
if self.pending_completion.is_some() {
cx.propagate();
return;
}
let message = self.composer_editor.update(cx, |composer_editor, cx| {
let text = composer_editor.text(cx);
let id = self.next_message_id.post_inc();
let body = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_text(text, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor
});
composer_editor.clear(cx);
ChatMessage::User(UserMessage {
id,
body,
attachments: Vec::new(),
})
});
self.push_message(message, cx);
} else {
log::error!("unexpected state: no user message editor is focused.");
return;
}
let mode = *mode;
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
let attachments_task = this.update(&mut cx, |this, cx| {
let attachment_store = this.attachment_registry.clone();
attachment_store.call_all_attachment_tools(cx)
});
let attachments = maybe!(async {
let attachments_task = attachments_task?;
let attachments = attachments_task.await?;
anyhow::Ok(attachments)
})
.await
.log_err()
.unwrap_or_default();
// Set the attachments to the _last_ user message
this.update(&mut cx, |this, _cx| {
if let Some(ChatMessage::User(message)) = this.messages.last_mut() {
message.attachments = attachments;
}
})
.log_err();
Self::request_completion(
this.clone(),
mode,
MAX_COMPLETION_CALLS_PER_SUBMISSION,
&mut cx,
)
.await
.log_err();
this.update(&mut cx, |this, _cx| {
this.pending_completion = None;
})
.context("Failed to push new user message")
.log_err();
}));
}
async fn request_completion(
this: WeakView<Self>,
mode: SubmitMode,
limit: usize,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let mut call_count = 0;
loop {
let complete = async {
let (tool_definitions, model_name, messages) = this.update(cx, |this, cx| {
this.push_new_assistant_message(cx);
let definitions = if call_count < limit
&& matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
{
this.tool_registry.definitions()
} else {
Vec::new()
};
call_count += 1;
(
definitions,
this.model.clone(),
this.completion_messages(cx),
)
})?;
let messages = messages.await?;
let completion = cx.update(|cx| {
CompletionProvider::get(cx).complete(
model_name,
messages,
Vec::new(),
1.0,
tool_definitions,
)
});
let mut stream = completion?.await?;
let mut body = String::new();
while let Some(delta) = stream.next().await {
let delta = delta?;
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage {
body: message_body,
tool_calls: message_tool_calls,
..
})) = this.messages.last_mut()
{
if let Some(content) = &delta.content {
body.push_str(content);
}
for tool_call in delta.tool_calls {
let index = tool_call.index as usize;
if index >= message_tool_calls.len() {
message_tool_calls.resize_with(index + 1, Default::default);
}
let call = &mut message_tool_calls[index];
if let Some(id) = &tool_call.id {
call.id.push_str(id);
}
match tool_call.variant {
Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
if let Some(name) = &tool_call.name {
call.name.push_str(name);
}
if let Some(arguments) = &tool_call.arguments {
call.arguments.push_str(arguments);
}
}
None => {}
}
}
*message_body =
RichText::new(body.clone(), &[], &this.language_registry);
cx.notify();
} else {
unreachable!()
}
})?;
}
anyhow::Ok(())
}
.await;
let mut tool_tasks = Vec::new();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage {
error: message_error,
tool_calls,
..
})) = this.messages.last_mut()
{
if let Err(error) = complete {
message_error.replace(SharedString::from(error.to_string()));
cx.notify();
} else {
for tool_call in tool_calls.iter() {
tool_tasks.push(this.tool_registry.call(tool_call, cx));
}
}
}
})?;
if tool_tasks.is_empty() {
return Ok(());
}
let tools = join_all(tool_tasks.into_iter()).await;
// If the WindowContext went away for any tool's view we don't include it
// especially since the below call would fail for the same reason.
let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) =
this.messages.last_mut()
{
*tool_calls = tools;
cx.notify();
}
})?;
}
}
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(),
body: RichText::default(),
tool_calls: Vec::new(),
error: None,
});
self.push_message(message, cx);
}
fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext<Self>) {
let old_len = self.messages.len();
let focus_handle = Some(message.focus_handle(cx));
self.messages.push(message);
self.list_state
.splice_focusable(old_len..old_len, focus_handle);
cx.notify();
}
fn pop_message(&mut self, cx: &mut ViewContext<Self>) {
if self.messages.is_empty() {
return;
}
self.messages.pop();
self.list_state
.splice(self.messages.len()..self.messages.len() + 1, 0);
cx.notify();
}
fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
if let Some(index) = self.messages.iter().position(|message| match message {
ChatMessage::User(message) => message.id == last_message_id,
ChatMessage::Assistant(message) => message.id == last_message_id,
}) {
self.list_state.splice(index + 1..self.messages.len(), 0);
self.messages.truncate(index + 1);
cx.notify();
}
}
fn is_message_collapsed(&self, id: &MessageId) -> bool {
self.collapsed_messages.get(id).copied().unwrap_or_default()
}
fn toggle_message_collapsed(&mut self, id: MessageId) {
let entry = self.collapsed_messages.entry(id).or_insert(false);
*entry = !*entry;
}
fn render_error(
&self,
error: Option<SharedString>,
_ix: usize,
cx: &mut ViewContext<Self>,
) -> AnyElement {
let theme = cx.theme();
if let Some(error) = error {
div()
.py_1()
.px_2()
.mx_neg_1()
.rounded_md()
.border_1()
.border_color(theme.status().error_border)
// .bg(theme.status().error_background)
.text_color(theme.status().error)
.child(error.clone())
.into_any_element()
} else {
div().into_any_element()
}
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let is_first = ix == 0;
let is_last = ix == self.messages.len() - 1;
let padding = Spacing::Large.rems(cx);
// Whenever there's a run of assistant messages, group as one Assistant UI element
match &self.messages[ix] {
ChatMessage::User(UserMessage {
id,
body,
attachments,
}) => div()
.id(SharedString::from(format!("message-{}-container", id.0)))
.when(is_first, |this| this.pt(padding))
.map(|element| {
if self.editing_message_id() == Some(*id) {
element.child(Composer::new(
body.clone(),
self.project_index_button.clone(),
self.active_file_button.clone(),
crate::ui::ModelSelector::new(
cx.view().downgrade(),
self.model.clone(),
)
.into_any_element(),
))
} else {
element
.on_click(cx.listener({
let id = *id;
let body = body.clone();
move |assistant_chat, event: &ClickEvent, cx| {
if event.up.click_count == 2 {
assistant_chat.editing_message = Some(EditingMessage {
id,
body: body.clone(),
old_body: body.read(cx).text(cx).into(),
});
body.focus_handle(cx).focus(cx);
}
}
}))
.child(
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
Some(
RichText::new(
body.read(cx).text(cx),
&[],
&self.language_registry,
)
.element(ElementId::from(id.0), cx),
),
Some(
h_flex()
.gap_2()
.children(
attachments
.iter()
.map(|attachment| attachment.view.clone()),
)
.into_any_element(),
),
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
)
// TODO: Wire up selections.
.selected(is_last),
)
}
})
.into_any(),
ChatMessage::Assistant(AssistantMessage {
id,
body,
error,
tool_calls,
..
}) => {
let assistant_body = if body.text.is_empty() {
None
} else {
Some(
div()
.child(body.element(ElementId::from(id.0), cx))
.into_any_element(),
)
};
let tools = tool_calls
.iter()
.map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
.collect::<Vec<AnyElement>>();
let tools_body = if tools.is_empty() {
None
} else {
Some(div().children(tools).into_any_element())
};
div()
.when(is_first, |this| this.pt(padding))
.child(
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::Assistant,
assistant_body,
tools_body,
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
)
// TODO: Wire up selections.
.selected(is_last),
)
.child(self.render_error(error.clone(), ix, cx))
.into_any()
}
}
}
fn completion_messages(&self, cx: &mut WindowContext) -> Task<Result<Vec<CompletionMessage>>> {
let project_index = self.project_index.read(cx);
let project = project_index.project();
let fs = project_index.fs();
let mut project_context = ProjectContext::new(project, fs);
let mut completion_messages = Vec::new();
for message in &self.messages {
match message {
ChatMessage::User(UserMessage {
body, attachments, ..
}) => {
for attachment in attachments {
if let Some(content) = attachment.generate(&mut project_context, cx) {
completion_messages.push(CompletionMessage::System { content });
}
}
// Show user's message last so that the assistant is grounded in the user's request
completion_messages.push(CompletionMessage::User {
content: body.read(cx).text(cx),
});
}
ChatMessage::Assistant(AssistantMessage {
body, tool_calls, ..
}) => {
// In no case do we want to send an empty message. This shouldn't happen, but we might as well
// not break the Chat API if it does.
if body.text.is_empty() && tool_calls.is_empty() {
continue;
}
let tool_calls_from_assistant = tool_calls
.iter()
.map(|tool_call| ToolCall {
content: ToolCallContent::Function {
function: FunctionContent {
name: tool_call.name.clone(),
arguments: tool_call.arguments.clone(),
},
},
id: tool_call.id.clone(),
})
.collect();
completion_messages.push(CompletionMessage::Assistant {
content: Some(body.text.to_string()),
tool_calls: tool_calls_from_assistant,
});
for tool_call in tool_calls {
// Every tool call _must_ have a result by ID, otherwise OpenAI will error.
let content = match &tool_call.result {
Some(result) => {
result.generate(&tool_call.name, &mut project_context, cx)
}
None => "".to_string(),
};
completion_messages.push(CompletionMessage::Tool {
content,
tool_call_id: tool_call.id.clone(),
});
}
}
}
}
let system_message = project_context.generate_system_message(cx);
cx.background_executor().spawn(async move {
let content = system_message.await?;
completion_messages.insert(0, CompletionMessage::System { content });
Ok(completion_messages)
})
}
}
impl Render for AssistantChat {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.relative()
.flex_1()
.v_flex()
.key_context("AssistantChat")
.on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::cancel))
.text_color(Color::Default.color(cx))
.child(list(self.list_state.clone()).flex_1())
.child(Composer::new(
self.composer_editor.clone(),
self.project_index_button.clone(),
self.active_file_button.clone(),
crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
.into_any_element(),
))
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct MessageId(usize);
impl MessageId {
fn post_inc(&mut self) -> Self {
let id = *self;
self.0 += 1;
id
}
}
enum ChatMessage {
User(UserMessage),
Assistant(AssistantMessage),
}
impl ChatMessage {
fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
match self {
ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
ChatMessage::Assistant(_) => None,
}
}
}
struct UserMessage {
id: MessageId,
body: View<Editor>,
attachments: Vec<UserAttachment>,
}
struct AssistantMessage {
id: MessageId,
body: RichText,
tool_calls: Vec<ToolFunctionCall>,
error: Option<SharedString>,
}

View File

@@ -0,0 +1,26 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Default, Debug, Deserialize, Serialize, Clone)]
pub struct AssistantSettings {
pub enabled: bool,
}
#[derive(Default, Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct AssistantSettingsContent {
pub enabled: Option<bool>,
}
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant_v2");
type FileContent = AssistantSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Ok(sources.json_merge().unwrap_or_else(|_| Default::default()))
}
}

View File

@@ -0,0 +1,114 @@
pub mod active_file;
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelAttachment, ProjectContext, ToolOutput};
use editor::Editor;
use gpui::{Render, Task, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectPath;
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
use util::maybe;
use workspace::Workspace;
pub struct ActiveEditorAttachment {
buffer: WeakModel<Buffer>,
path: Option<ProjectPath>,
}
pub struct FileAttachmentView {
output: Result<ActiveEditorAttachment>,
}
impl Render for FileAttachmentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(attachment) => {
let filename: SharedString = attachment
.path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
// todo!(): make the button link to the actual file to open
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip({
move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx)
})
.into_any_element()
}
Err(err) => div().child(err.to_string()).into_any_element(),
}
}
}
impl ToolOutput for FileAttachmentView {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
if let Ok(result) = &self.output {
if let Some(path) = &result.path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
} else if let Some(buffer) = result.buffer.upgrade() {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
}
String::new()
}
}
pub struct ActiveEditorAttachmentTool {
workspace: WeakView<Workspace>,
}
impl ActiveEditorAttachmentTool {
pub fn new(workspace: WeakView<Workspace>, _cx: &mut WindowContext) -> Self {
Self { workspace }
}
}
impl LanguageModelAttachment for ActiveEditorAttachmentTool {
type Output = ActiveEditorAttachment;
type View = FileAttachmentView;
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
Task::ready(maybe!({
let active_buffer = self
.workspace
.update(cx, |workspace, cx| {
workspace
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()))
})?
.ok_or_else(|| anyhow!("no active buffer"))?;
let buffer = active_buffer.read(cx);
if let Some(buffer) = buffer.as_singleton() {
let path =
project::File::from_dyn(buffer.read(cx).file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
return Ok(ActiveEditorAttachment {
buffer: buffer.downgrade(),
path,
});
} else {
Err(anyhow!("no active buffer"))
}
}))
}
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| FileAttachmentView { output })
}
}

View File

@@ -0,0 +1,183 @@
use anyhow::Result;
use assistant_tooling::ToolFunctionDefinition;
use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AppContext, Global};
use std::sync::Arc;
pub use open_ai::RequestMessage as CompletionMessage;
#[derive(Clone)]
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
impl CompletionProvider {
pub fn get(cx: &AppContext) -> &Self {
cx.global::<CompletionProvider>()
}
pub fn new(backend: impl CompletionProviderBackend) -> Self {
Self(Arc::new(backend))
}
pub fn default_model(&self) -> String {
self.0.default_model()
}
pub fn available_models(&self) -> Vec<String> {
self.0.available_models()
}
pub fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
{
self.0.complete(model, messages, stop, temperature, tools)
}
}
impl Global for CompletionProvider {}
pub trait CompletionProviderBackend: 'static {
fn default_model(&self) -> String;
fn available_models(&self) -> Vec<String>;
fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>;
}
pub struct CloudCompletionProvider {
client: Arc<Client>,
}
impl CloudCompletionProvider {
pub fn new(client: Arc<Client>) -> Self {
Self { client }
}
}
impl CompletionProviderBackend for CloudCompletionProvider {
fn default_model(&self) -> String {
"gpt-4-turbo".into()
}
fn available_models(&self) -> Vec<String> {
vec!["gpt-4-turbo".into(), "gpt-4".into(), "gpt-3.5-turbo".into()]
}
fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
{
let client = self.client.clone();
let tools: Vec<proto::ChatCompletionTool> = tools
.iter()
.filter_map(|tool| {
Some(proto::ChatCompletionTool {
variant: Some(proto::chat_completion_tool::Variant::Function(
proto::chat_completion_tool::FunctionObject {
name: tool.name.clone(),
description: Some(tool.description.clone()),
parameters: Some(serde_json::to_string(&tool.parameters).ok()?),
},
)),
})
})
.collect();
let tool_choice = match tools.is_empty() {
true => None,
false => Some("auto".into()),
};
async move {
let stream = client
.request_stream(proto::CompleteWithLanguageModel {
model,
messages: messages
.into_iter()
.map(|message| match message {
CompletionMessage::Assistant {
content,
tool_calls,
} => proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelAssistant as i32,
content: content.unwrap_or_default(),
tool_call_id: None,
tool_calls: tool_calls
.into_iter()
.map(|tool_call| match tool_call.content {
open_ai::ToolCallContent::Function { function } => {
proto::ToolCall {
id: tool_call.id,
variant: Some(proto::tool_call::Variant::Function(
proto::tool_call::FunctionCall {
name: function.name,
arguments: function.arguments,
},
)),
}
}
})
.collect(),
},
CompletionMessage::User { content } => {
proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelUser as i32,
content,
tool_call_id: None,
tool_calls: Vec::new(),
}
}
CompletionMessage::System { content } => {
proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelSystem as i32,
content,
tool_calls: Vec::new(),
tool_call_id: None,
}
}
CompletionMessage::Tool {
content,
tool_call_id,
} => proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelTool as i32,
content,
tool_call_id: Some(tool_call_id),
tool_calls: Vec::new(),
},
})
.collect(),
stop,
temperature,
tool_choice,
tools,
})
.await?;
Ok(stream
.filter_map(|response| async move {
match response {
Ok(mut response) => Some(Ok(response.choices.pop()?.delta?)),
Err(error) => Some(Err(error)),
}
})
.boxed())
}
.boxed()
}
}

View File

@@ -0,0 +1,5 @@
mod create_buffer;
mod project_index;
pub use create_buffer::*;
pub use project_index::*;

View File

@@ -0,0 +1,119 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
use editor::Editor;
use gpui::{prelude::*, Model, Task, View, WeakView};
use project::Project;
use schemars::JsonSchema;
use serde::Deserialize;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
pub struct CreateBufferTool {
workspace: WeakView<Workspace>,
project: Model<Project>,
}
impl CreateBufferTool {
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self { workspace, project }
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateBufferInput {
/// The contents of the buffer.
text: String,
/// The name of the language to use for the buffer.
///
/// This should be a human-readable name, like "Rust", "JavaScript", or "Python".
language: String,
}
impl LanguageModelTool for CreateBufferTool {
type Input = CreateBufferInput;
type Output = ();
type View = CreateBufferView;
fn name(&self) -> String {
"create_buffer".to_string()
}
fn description(&self) -> String {
"Create a new buffer in the current codebase".to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
cx.spawn({
let workspace = self.workspace.clone();
let project = self.project.clone();
let text = input.text.clone();
let language_name = input.language.clone();
|mut cx| async move {
let language = cx
.update(|cx| {
project
.read(cx)
.languages()
.language_for_name(&language_name)
})?
.await?;
let buffer = cx
.update(|cx| project.update(cx, |project, cx| project.create_buffer(cx)))?
.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.edit([(0..0, text)], None, cx);
buffer.set_language(Some(language), cx)
})?;
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project), cx)),
),
None,
cx,
);
})
.log_err();
Ok(())
}
})
}
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {
language: input.language,
output,
})
}
}
pub struct CreateBufferView {
language: String,
output: Result<()>,
}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child("Opening a buffer")
}
}
impl ToolOutput for CreateBufferView {
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
match &self.output {
Ok(_) => format!("Created a new {} buffer", self.language),
Err(err) => format!("Failed to create buffer: {err:?}"),
}
}
}

View File

@@ -0,0 +1,209 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ToolOutput};
use collections::BTreeMap;
use gpui::{prelude::*, Model, Task};
use project::ProjectPath;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::Deserialize;
use std::{fmt::Write as _, ops::Range};
use ui::{div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
const DEFAULT_SEARCH_LIMIT: usize = 20;
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
}
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
#[derive(Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Maximum number of results to return, defaults to 20
limit: Option<usize>,
}
pub struct ProjectIndexView {
input: CodebaseQuery,
output: Result<ProjectIndexOutput>,
element_id: ElementId,
expanded_header: bool,
}
pub struct ProjectIndexOutput {
status: Status,
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
}
impl ProjectIndexView {
fn new(input: CodebaseQuery, output: Result<ProjectIndexOutput>) -> Self {
let element_id = ElementId::Name(nanoid::nanoid!().into());
Self {
input,
output,
element_id,
expanded_header: false,
}
}
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
self.expanded_header = !self.expanded_header;
cx.notify();
}
}
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let result = &self.output;
let output = match result {
Err(err) => {
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
}
Ok(output) => output,
};
let file_count = output.excerpts.len();
let header = h_flex()
.gap_2()
.child(Icon::new(IconName::File))
.child(format!(
"Read {} {}",
file_count,
if file_count == 1 { "file" } else { "files" }
));
v_flex().gap_3().child(
CollapsibleContainer::new(self.element_id.clone(), self.expanded_header)
.start_slot(header)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_header(cx);
}))
.child(
v_flex()
.gap_3()
.p_3()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.child(
v_flex()
.gap_2()
.children(output.excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string())
.color(Color::Muted),
)
})),
),
),
)
}
}
impl ToolOutput for ProjectIndexView {
fn generate(
&self,
context: &mut assistant_tooling::ProjectContext,
_: &mut WindowContext,
) -> String {
match &self.output {
Ok(output) => {
let mut body = "found results in the following paths:\n".to_string();
for (project_path, ranges) in &output.excerpts {
context.add_excerpts(project_path.clone(), ranges);
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
}
if output.status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
body
}
Err(err) => format!("Error: {}", err),
}
}
}
impl ProjectIndexTool {
pub fn new(project_index: Model<ProjectIndex>) -> Self {
Self { project_index }
}
}
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = ProjectIndexOutput;
type View = ProjectIndexView;
fn name(&self) -> String {
"query_codebase".to_string()
}
fn description(&self) -> String {
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of code chunks in the code base and an embedding of the query.".to_string()
}
fn execute(&self, query: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
let project_index = self.project_index.read(cx);
let status = project_index.status();
let search = project_index.search(
query.query.clone(),
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
cx.spawn(|mut cx| async move {
let search_results = search.await?;
cx.update(|cx| {
let mut output = ProjectIndexOutput {
status,
excerpts: Default::default(),
};
for search_result in search_results {
let path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path.clone(),
};
let excerpts_for_path = output.excerpts.entry(path).or_default();
let ix = match excerpts_for_path
.binary_search_by_key(&search_result.range.start, |r| r.start)
{
Ok(ix) | Err(ix) => ix,
};
excerpts_for_path.insert(ix, search_result.range);
}
output
})
})
}
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| ProjectIndexView::new(input, output))
}
fn render_running(_: &mut WindowContext) -> impl IntoElement {
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false)
.start_slot("Searching code base")
}
}

View File

@@ -0,0 +1,17 @@
mod active_file_button;
mod chat_message;
mod chat_notice;
mod composer;
mod project_index_button;
#[cfg(feature = "stories")]
mod stories;
pub use active_file_button::*;
pub use chat_message::*;
pub use chat_notice::*;
pub use composer::*;
pub use project_index_button::*;
#[cfg(feature = "stories")]
pub use stories::*;

Some files were not shown because too many files have changed in this diff Show More