Compare commits

..

13 Commits

Author SHA1 Message Date
Nathan Sobo
6764d2b600 zed 0.137.5 2024-05-29 17:08:25 -06:00
Nathan Sobo
9cd3d26a15 Make prompt library icon in context panel staff-only for now (#12457)
This is still pretty raw, so I'd like to hold off on shipping it to all
users.

Release Notes:

- Hide the prompt library for non-staff until it is in a more complete
state.
2024-05-29 17:06:31 -06:00
Max Brunsfeld
1f25391366 zed 0.137.3 2024-05-29 11:15:38 -07:00
Bennet Bo Fenner
360cc36a8d Fix deleted hunk offset when zooming (#12442)
Release Notes:

- Fixed an issue where expanded hunks could be rendered at the wrong
position when zooming
- Fixed an issue where expanded hunks could be rendered at the wrong
position when toggling git blame
([#11941](https://github.com/zed-industries/zed/issues/11941))
2024-05-29 11:13:30 -07:00
Joseph T. Lyons
951898034c v0.137.x stable 2024-05-29 12:15:00 -04:00
gcp-cherry-pick-bot[bot]
6bf53a1fcd Tweak client reconnect timing (cherry-pick #12393) (#12438)
Cherry-picked Tweak client reconnect timing (#12393)

* Start with a longer duration
* Widen the range used for randomizing the duration between retries
* Increase the maximum duration between retries

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2024-05-29 09:01:45 -07:00
Zed Bot
719680b5a7 Bump to 0.137.2 for @ConradIrwin 2024-05-23 21:13:35 -07:00
gcp-cherry-pick-bot[bot]
944356ec10 Make reconnects smoother for dev servers (cherry-pick #12223) (#12226)
Cherry-picked Make reconnects smoother for dev servers (#12223)

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Nathan <nathan@zed.dev>
2024-05-23 22:13:10 -06:00
Zed Bot
e5646fb28d Bump to 0.137.1 for @ConradIrwin 2024-05-22 21:52:36 -07:00
gcp-cherry-pick-bot[bot]
44a5fdc425 remote config fixes (cherry-pick #12178) (#12181)
Cherry-picked remote config fixes (#12178)

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-22 22:51:52 -06:00
gcp-cherry-pick-bot[bot]
21ea30122a reconnect ssh (cherry-pick #12147) (#12179)
Cherry-picked reconnect ssh (#12147)

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Bennet <bennet@zed.dev>
2024-05-22 22:43:03 -06:00
gcp-cherry-pick-bot[bot]
416fa58850 remoting fixes (cherry-pick #12137) (#12146)
Cherry-picked remoting fixes (#12137)

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-22 13:16:36 -06:00
Joseph T. Lyons
cd2ec1e08e v0.137.x preview 2024-05-22 11:26:43 -04:00
669 changed files with 20625 additions and 51363 deletions

15
.cargo/ci-config.toml Normal file
View File

@@ -0,0 +1,15 @@
# This config is different from config.toml in this directory, as the latter is recognized by Cargo.
# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml
# with preference for settings from Zeds config.toml.
# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence.
# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file
# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml`
# would be incovenient.
# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables
# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it.
[build]
rustflags = ["-D", "warnings"]
[alias]
xtask = "run --package xtask --"

View File

@@ -1,5 +0,0 @@
# This file is used to build collab in a Docker image.
# In particular, we don't use clang.
[build]
# v0 mangling scheme provides more detailed backtraces around closures
rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]

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

@@ -2,23 +2,23 @@ name: Feature Request
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
labels: ["admin read", "triage", "enhancement"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the feature
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe the feature
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: |
If applicable, add mockups / screenshots to help present your vision of the feature
description: Drag images into the text input below
validations:
required: false
- type: textarea
attributes:
label: |
If applicable, add mockups / screenshots to help present your vision of the feature
description: Drag images into the text input below
validations:
required: false

View File

@@ -1,45 +1,40 @@
name: Bug Report
description: |
Use this template for **non-crash-related** bug reports.
Tip: open this issue template from within Zed with the `file bug report` command palette action.
Use this template for **non-crash-related** bug reports.
Tip: open this issue template from within Zed with the `file bug report` command palette action.
labels: ["admin read", "triage", "defect"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
validations:
required: false
- type: textarea
attributes:
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
description: |
Drag Zed.log into the text input below.
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
value: |
<details><summary>Zed.log</summary><pre>
<!-- Click below this line and paste or drag-and-drop your log-->
<!-- Click above this line and paste or drag-and-drop your log--></pre></details>
validations:
required: false
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
validations:
required: false
- type: textarea
attributes:
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
description: |
Drag Zed.log into the text input below.
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
validations:
required: false

View File

@@ -1,38 +1,33 @@
name: Crash Report
description: |
Use this template for crash reports.
Use this template for crash reports.
labels: ["admin read", "triage", "defect", "panic / crash"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
description: |
Drag Zed.log into the text input below.
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
value: |
<details><summary>Zed.log</summary><pre>
<!-- Click below this line and paste or drag-and-drop your log-->
<!-- Click above this line and paste or drag-and-drop your log--></pre></details>
validations:
required: false
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
description: |
Drag Zed.log into the text input below.
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
validations:
required: false

View File

@@ -23,7 +23,6 @@ env:
jobs:
style:
timeout-minutes: 60
name: Check formatting and spelling
runs-on:
- self-hosted
@@ -38,6 +37,9 @@ jobs:
- name: Remove untracked files
run: git clean -df
- name: Set up default .cargo/config.toml
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
- name: Check spelling
run: |
if ! which typos > /dev/null; then
@@ -51,9 +53,6 @@ jobs:
- name: Check unused dependencies
uses: bnjbvr/cargo-machete@main
- name: Check licenses are present
run: script/check-licenses
- name: Check license generation
run: script/generate-licenses /tmp/zed_licenses_output
@@ -74,11 +73,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
@@ -90,7 +88,7 @@ jobs:
clean: false
- name: cargo clippy
run: ./script/clippy
run: cargo xtask clippy
- name: Run tests
uses: ./.github/actions/run_tests
@@ -101,8 +99,8 @@ 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
@@ -117,17 +115,13 @@ jobs:
clean: false
- name: cargo clippy
run: ./script/clippy
- name: Run tests
uses: ./.github/actions/run_tests
run: cargo xtask clippy
- 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
steps:
@@ -142,13 +136,12 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo clippy
run: ./script/clippy
run: cargo xtask clippy
- name: Build Zed
run: cargo build -p zed
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
runs-on:
- self-hosted
@@ -259,7 +252,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux:
timeout-minutes: 60
name: Create a Linux bundle
runs-on:
- self-hosted
@@ -307,7 +299,10 @@ jobs:
exit 1
fi
- name: Create Linux .tar.gz bundle
- name: Generate license file
run: script/generate-licenses
- 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
@@ -315,7 +310,7 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
path: zed-*.tar.gz
- name: Upload app bundle to release
uses: softprops/action-gh-release@v1
@@ -327,86 +322,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 libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@v1
with:
mold-version: 2.32.0
- name: rustup
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: 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

@@ -27,7 +27,7 @@ jobs:
uses: ./.github/actions/check_style
- name: Run clippy
run: ./script/clippy
run: cargo xtask clippy
tests:
name: Run tests
@@ -75,9 +75,6 @@ jobs:
with:
clean: false
- name: Set up default .cargo/config.toml
run: cp ./.cargo/collab-config.toml ./.cargo/config.toml
- name: Build docker image
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA

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:
@@ -32,9 +31,8 @@ jobs:
uses: ./.github/actions/check_style
- name: Run clippy
run: ./script/clippy
run: cargo xtask clippy
tests:
timeout-minutes: 60
name: Run tests
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -51,7 +49,6 @@ jobs:
uses: ./.github/actions/run_tests
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -94,7 +91,6 @@ 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:

3
.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
@@ -27,4 +25,3 @@ DerivedData/
.blob_store
.vscode
.wrangler
.flatpak-builder

View File

@@ -9,18 +9,12 @@
# Keep these entries sorted alphabetically.
# In Zed: `editor: sort lines case sensitive`
Alex Viscreanu <alexviscreanu@gmail.com>
Alex Viscreanu <alexviscreanu@gmail.com> <alexandru.viscreanu@kiwi.com>
Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Bennet Bo Fenner <bennet@zed.dev>
Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
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>
Evren Sen <146845123+evrsen@users.noreply.github.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>
@@ -54,23 +48,12 @@ 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>
Peter Tripp <peter@zed.dev>
Peter Tripp <peter@zed.dev> <petertripp@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>
Rashid Almheiri <r.muhairi@pm.me>
Rashid Almheiri <r.muhairi@pm.me> <69181766+huwaireb@users.noreply.github.com>
Richard Feldman <oss@rtfeldman.com>
Richard Feldman <oss@rtfeldman.com> <richard@zed.dev>
Robert Clover <git@clo4.net>
Robert Clover <git@clo4.net> <robert@clover.gdn>
Sergey Onufrienko <sergey@onufrienko.com>
Thorsten Ball <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
WindSoilder <WindSoilder@outlook.com>
张小白 <364772080@qq.com>

View File

@@ -1,7 +1,7 @@
[
{
"label": "clippy",
"command": "./script/clippy",
"args": []
"command": "cargo",
"args": ["xtask", "clippy"]
}
]

1478
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ members = [
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant_slash_command",
"crates/assistant2",
"crates/assistant_tooling",
"crates/audio",
"crates/auto_update",
@@ -41,7 +41,6 @@ members = [
"crates/gpui",
"crates/gpui_macros",
"crates/headless",
"crates/html_to_markdown",
"crates/http",
"crates/image_viewer",
"crates/inline_completion_button",
@@ -61,28 +60,22 @@ members = [
"crates/multi_buffer",
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
"crates/open_ai",
"crates/outline",
"crates/outline_panel",
"crates/paths",
"crates/picker",
"crates/prettier",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/proto",
"crates/quick_action_bar",
"crates/recent_projects",
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/dev_server_projects",
"crates/repl",
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rustdoc",
"crates/task",
"crates/tasks_ui",
"crates/search",
@@ -136,7 +129,6 @@ members = [
"extensions/prisma",
"extensions/purescript",
"extensions/ruby",
"extensions/snippets",
"extensions/svelte",
"extensions/terraform",
"extensions/toml",
@@ -155,9 +147,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"
@@ -170,10 +161,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" }
@@ -193,7 +184,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" }
@@ -213,17 +203,13 @@ 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" }
paths = { path = "crates/paths" }
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" }
@@ -231,11 +217,9 @@ quick_action_bar = { path = "crates/quick_action_bar" }
recent_projects = { path = "crates/recent_projects" }
release_channel = { path = "crates/release_channel" }
dev_server_projects = { path = "crates/dev_server_projects" }
repl = { path = "crates/repl" }
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" }
@@ -269,32 +253,27 @@ workspace = { path = "crates/workspace" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
alacritty_terminal = "0.23"
anyhow = "1.0.57"
any_vec = "0.13"
ashpd = "0.8.0"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = { version = "0.1"}
async-fs = "1.6"
async-recursion = "1.0.0"
async-tar = "0.4.2"
async-trait = "0.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
blade-util = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
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"
core-foundation = { version = "0.9.3" }
core-foundation-sys = "0.8.6"
derive_more = "0.99.17"
dirs = "4.0"
emojis = "0.6.1"
env_logger = "0.9"
exec = "0.3.1"
@@ -302,31 +281,30 @@ fork = "0.1.23"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.19", default-features = false }
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"
image = "0.25.1"
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"
@@ -338,7 +316,6 @@ rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
repair_json = "0.1.0"
runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] }
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
schemars = "0.8"
@@ -354,8 +331,6 @@ serde_repr = "0.1"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
signal-hook = "0.3.17"
similar = "1.3"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -375,33 +350,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-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 = [
@@ -417,13 +392,12 @@ wit-component = "0.201"
sys-locale = "0.3.1"
[workspace.dependencies.windows]
version = "0.57"
version = "0.56.0"
features = [
"implement",
"Foundation_Numerics",
"System",
"System_Threading",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
@@ -459,12 +433,11 @@ features = [
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7b4894ba2ae81b988846676f54c0988d4027ef4f" }
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "4968e819c0d9b015437ffc694511e175801a17c7" }
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" }
[profile.dev]
split-debuginfo = "unpacked"
debug = "limited"
codegen-units = 16
[profile.dev.package]
taffy = { opt-level = 3 }
@@ -483,11 +456,6 @@ codegen-units = 1
[profile.release.package]
zed = { codegen-units = 16 }
[profile.release-fast]
inherits = "release"
lto = false
codegen-units = 16
[workspace.lints.clippy]
dbg_macro = "deny"
todo = "deny"

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,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",
@@ -61,12 +61,12 @@
"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 +74,6 @@
"ib": "storage",
"ico": "image",
"ini": "settings",
"inl": "cpp",
"j2k": "image",
"java": "java",
"jfif": "image",
@@ -82,9 +81,9 @@
"jpeg": "image",
"jpg": "image",
"js": "javascript",
"jsx": "react",
"json": "storage",
"jsonc": "storage",
"jsx": "react",
"jxl": "image",
"kt": "kotlin",
"ldf": "storage",
@@ -99,9 +98,9 @@
"mdf": "storage",
"mdx": "document",
"metadata": "code",
"mkv": "video",
"mjs": "javascript",
"mka": "audio",
"mkv": "video",
"ml": "ocaml",
"mli": "ocaml",
"mov": "video",
@@ -110,8 +109,8 @@
"mts": "typescript",
"myd": "storage",
"myi": "storage",
"nim": "nim",
"nu": "terminal",
"nim": "nim",
"odp": "document",
"ods": "document",
"odt": "document",
@@ -133,33 +132,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 +190,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 +247,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 +274,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 +316,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 +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-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>

Before

Width:  |  Height:  |  Size: 302 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 +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-cursor"><path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4v-1"/><path d="M7 2h1a4 4 0 0 1 4 4v1"/></svg>

Before

Width:  |  Height:  |  Size: 345 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

@@ -25,8 +25,7 @@
],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
}
},
{

View File

@@ -1,4 +1,5 @@
[
// todo(linux): Review the editor bindings
// Standard Linux bindings
{
"bindings": {
@@ -27,6 +28,7 @@
"ctrl-0": "zed::ResetBufferFontSize",
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"alt-f9": "zed::Hide",
"f11": "zed::ToggleFullScreen"
}
},
@@ -42,30 +44,40 @@
"shift-tab": "editor::TabPrev",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-t": "editor::Transpose",
// "ctrl-backspace": "editor::DeleteToBeginningOfLine",
// "ctrl-delete": "editor::DeleteToEndOfLine",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
// "ctrl-w": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
// "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-insert": "editor::Copy",
"ctrl-v": "editor::Paste",
"ctrl-y": "editor::Redo",
"shift-insert": "editor::Paste",
"ctrl-z": "editor::Undo",
"ctrl-shift-z": "editor::Redo",
"up": "editor::MoveUp",
"ctrl-up": "editor::LineUp",
"ctrl-down": "editor::LineDown",
// "ctrl-up": "editor::MoveToStartOfParagraph", todo(linux) Should be "scroll down by 1 line"
"pageup": "editor::PageUp",
"shift-pageup": "editor::SelectPageUp",
// "shift-pageup": "editor::MovePageUp", todo(linux) should be 'select page up'
"home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown",
// "ctrl-down": "editor::MoveToEndOfParagraph", todo(linux) should be "scroll up by 1 line"
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::SelectPageDown",
// "shift-pagedown": "editor::MovePageDown", todo(linux) should be 'select page down'
"end": "editor::MoveToEndOfLine",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
"ctrl-left": "editor::MoveToPreviousWordStart",
// "alt-b": "editor::MoveToPreviousWordStart",
"ctrl-right": "editor::MoveToNextWordEnd",
// "alt-f": "editor::MoveToNextWordEnd",
// "cmd-left": "editor::MoveToBeginningOfLine",
// "ctrl-a": "editor::MoveToBeginningOfLine",
// "cmd-right": "editor::MoveToEndOfLine",
// "ctrl-e": "editor::MoveToEndOfLine",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"shift-up": "editor::SelectUp",
@@ -76,6 +88,8 @@
"ctrl-shift-right": "editor::SelectToNextWordEnd",
"ctrl-shift-up": "editor::AddSelectionAbove",
"ctrl-shift-down": "editor::AddSelectionBelow",
// "ctrl-shift-up": "editor::SelectToStartOfParagraph",
// "ctrl-shift-down": "editor::SelectToEndOfParagraph",
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-a": "editor::SelectAll",
@@ -152,8 +166,7 @@
// "focus": false
// }
// ],
"ctrl->": "assistant::QuoteSelection",
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
"ctrl->": "assistant::QuoteSelection"
}
},
{
@@ -188,15 +201,17 @@
"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"
}
},
{
@@ -208,8 +223,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"
}
},
{
@@ -273,7 +287,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",
@@ -283,13 +296,6 @@
"ctrl-alt-shift-x": "search::ToggleRegex"
}
},
{
"context": "Terminal",
"bindings": {
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"]
}
},
// Bindings from VS Code
{
"context": "Editor",
@@ -414,7 +420,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",
@@ -434,7 +440,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",
@@ -540,37 +545,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": {
@@ -589,13 +569,10 @@
"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"
}
},
{
@@ -656,7 +633,12 @@
"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

@@ -176,20 +176,13 @@
"replace_enabled": true
}
],
"cmd-alt-l": [
"buffer_search::Deploy",
{
"selection_search_enabled": true
}
],
"cmd-e": [
"buffer_search::Deploy",
{
"focus": false
}
],
"cmd->": "assistant::QuoteSelection",
"cmd-alt-e": "editor::SelectEnclosingSymbol"
"cmd->": "assistant::QuoteSelection"
}
},
{
@@ -221,31 +214,20 @@
}
},
{
"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"
}
},
{
@@ -257,8 +239,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"
}
},
{
@@ -324,7 +305,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",
@@ -476,7 +456,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",
@@ -586,19 +565,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": {
@@ -612,15 +578,12 @@
"cmd-alt-c": "project_panel::CopyPath",
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"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"
}
},
{
@@ -670,7 +633,7 @@
{
"context": "Picker",
"bindings": {
"f2": "picker::UseSelectedQuery",
"alt-e": "picker::UseSelectedQuery",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
}

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

@@ -39,7 +39,6 @@
"right": "vim::Right",
"space": "vim::Space",
"$": "vim::EndOfLine",
"end": "vim::EndOfLine",
"^": "vim::FirstNonWhitespace",
"_": "vim::StartOfLineDownward",
"g _": "vim::EndOfLineDownward",
@@ -81,7 +80,6 @@
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
"/": "vim::Search",
"g /": "pane::DeploySearch",
"?": [
"vim::Search",
{
@@ -141,8 +139,7 @@
"ctrl-q": "vim::ToggleVisualBlock",
"shift-k": "editor::Hover",
"shift-r": "vim::ToggleReplace",
"0": "vim::StartOfLine",
"home": "vim::StartOfLine",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"ctrl-f": "vim::PageDown",
"pagedown": "vim::PageDown",
"ctrl-b": "vim::PageUp",
@@ -158,7 +155,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",
@@ -248,10 +244,9 @@
"displayLines": true
}
],
"g v": "vim::RestoreVisualSelection",
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
"g i": "vim::InsertAtPrevious",
"g i": ["workspace::SendKeystrokes", "` ^ i"],
"g ,": "vim::ChangeListNewer",
"g ;": "vim::ChangeListOlder",
"shift-h": "vim::WindowTop",
@@ -383,34 +378,25 @@
"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"
}
},
{
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
"\"": ["vim::PushOperator", "Register"],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
},
{
"context": "Editor && VimCount && vim_mode != insert",
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
},
{
"context": "Editor && VimCount",
"bindings": {
"0": ["vim::Number", 0]
}
@@ -439,27 +425,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": {
@@ -489,18 +454,6 @@
"s": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == >",
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == <",
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
@@ -540,8 +493,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",
@@ -610,15 +563,15 @@
}
},
{
"context": "Editor && vim_mode == normal && !VimWaiting",
"context": "Editor && vim_mode == normal",
"bindings": {
"g c c": "vim::ToggleComments"
"g c c": "editor::ToggleComments"
}
},
{
"context": "Editor && vim_mode == visual",
"bindings": {
"g c": "vim::ToggleComments"
"g c": "editor::ToggleComments"
}
},
{
@@ -636,7 +589,8 @@
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
"ctrl-r": ["vim::PushOperator", "Register"]
"ctrl-r \"": "editor::Paste",
"ctrl-r +": "editor::Paste"
}
},
{
@@ -645,13 +599,11 @@
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore",
"tab": "vim::Tab",
"enter": "vim::Enter",
"backspace": "vim::UndoReplace"
}
},
{
"context": "Editor && vim_mode != replace && VimWaiting",
"context": "Editor && VimWaiting",
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
@@ -659,13 +611,6 @@
"ctrl-[": ["vim::SwitchMode", "Normal"]
}
},
{
"context": "Editor && vim_mode == insert && VimWaiting",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore"
}
},
{
"context": "BufferSearchBar && !in_replace",
"bindings": {

View File

@@ -37,8 +37,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 +55,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
@@ -119,17 +115,15 @@
// 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*",
@@ -146,19 +140,15 @@
// opening parenthesis, bracket, brace, single or double quote characters.
// For example, when you type (, Zed will add a closing ) at the correct position.
"use_autoclose": true,
// Whether to automatically surround selected text when typing opening parenthesis,
// bracket, brace, single or double quote characters.
// For example, when you select text and type (, Zed will surround the text with ().
"use_auto_surround": true,
// Controls how the editor handles the autoclosed characters.
// When set to `false`(default), skipping over and auto-removing of the closing characters
// happen only for auto-inserted characters.
// Otherwise(when `true`), the closing characters are always skipped over and auto-removed
// no matter how they were inserted.
"always_treat_brackets_as_autoclosed": false,
// Controls whether inline completions are shown immediately (true)
// or manually by triggering `editor::ShowInlineCompletion` (false).
"show_inline_completions": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,
// Whether to show tabs and spaces in the editor.
// This setting can take three values:
//
@@ -168,28 +158,20 @@
// "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": {
// Whether to show breadcrumbs.
"breadcrumbs": true,
// Whether to show quick action buttons.
"quick_actions": true,
// Whether to show the Selections menu in the editor toolbar
"selections_menu": true
"quick_actions": true
},
// Scrollbar related settings
"scrollbar": {
@@ -231,34 +213,9 @@
"line_numbers": true,
// Whether to show code action buttons in the gutter.
"code_actions": true,
// Whether to show runnables buttons in the gutter.
"runnables": true,
// 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,
/// The width of the active indent guide in pixels, between 1 and 10.
"active_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"
},
// Whether the editor will scroll beyond the last line.
"scroll_beyond_last_line": "one_page",
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
// Scroll sensitivity multiplier. This multiplier is applied
@@ -312,37 +269,7 @@
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": false,
/// Scrollbar-related settings
"scrollbar": {
/// When to show the scrollbar in the project panel.
///
/// Default: always
"show": "always"
}
},
"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
"auto_fold_dirs": false
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
@@ -389,7 +316,7 @@
// 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"
@@ -403,9 +330,6 @@
"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.
@@ -477,16 +401,16 @@
// or falling back to formatting via language server:
// "formatter": "auto"
"formatter": "auto",
// How to soft-wrap long lines of text.
// Possible values:
// How to soft-wrap long lines of text. This setting can take
// three values:
//
// 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.
// 3. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 4. Soft wrap lines at the preferred line length.
// 4. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "prefer_line",
// The column at which to soft-wrap lines, for buffers where soft-wrap
@@ -504,8 +428,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": {
@@ -542,8 +465,9 @@
// "delay_ms": 600
}
},
"inline_completions": {
// A list of globs representing files that inline completions should be disabled for.
"copilot": {
// The set of glob patterns for which copilot should be disabled
// in any matching file.
"disabled_globs": [".env"]
},
// Settings specific to journaling
@@ -723,6 +647,9 @@
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Gleam": {
"tab_size": 2
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
@@ -748,7 +675,6 @@
}
},
"JavaScript": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"prettier": {
"allowed": true
}
@@ -758,6 +684,9 @@
"allowed": true
}
},
"Make": {
"hard_tabs": true
},
"Markdown": {
"format_on_save": "off",
"prettier": {
@@ -770,6 +699,9 @@
"plugins": ["@prettier/plugin-php"]
}
},
"Prisma": {
"tab_size": 2
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "..."]
},
@@ -791,7 +723,6 @@
}
},
"TSX": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"prettier": {
"allowed": true
}
@@ -802,7 +733,6 @@
}
},
"TypeScript": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"prettier": {
"allowed": true
}
@@ -906,7 +836,7 @@
// environment variables.
//
// Examples:
// - "proxy": "socks5://localhost:10808"
// - "proxy": "http://127.0.0.1:10809"
// - "proxy" = "socks5://localhost:10808"
// - "proxy" = "http://127.0.0.1:10809"
"proxy": null
}

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

@@ -3,22 +3,22 @@ use editor::Editor;
use extension::ExtensionStore;
use futures::StreamExt;
use gpui::{
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
VisualContext as _,
};
use language::{
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
ViewContext, VisualContext as _,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ContextMenu};
use std::{cmp::Reverse, fmt::Write, sync::Arc};
use ui::prelude::*;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
const DOWNLOAD_ICON: &str = "icons/download.svg";
const WARNING_ICON: &str = "icons/warning.svg";
pub enum Event {
ShowError { lsp_name: Arc<str>, error: String },
}
@@ -27,7 +27,6 @@ pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>,
context_menu: Option<View<ContextMenu>>,
}
struct LspStatus {
@@ -36,14 +35,14 @@ struct LspStatus {
}
struct PendingWork<'a> {
language_server_id: LanguageServerId,
language_server_name: &'a str,
progress_token: &'a str,
progress: &'a LanguageServerProgress,
}
#[derive(Default)]
struct Content {
icon: Option<gpui::AnyElement>,
icon: Option<&'static str>,
message: String,
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
}
@@ -79,7 +78,6 @@ impl ActivityIndicator {
statuses: Default::default(),
project: project.clone(),
auto_updater,
context_menu: None,
}
});
@@ -153,7 +151,7 @@ impl ActivityIndicator {
.read(cx)
.language_server_statuses()
.rev()
.filter_map(|(server_id, status)| {
.filter_map(|status| {
if status.pending_work.is_empty() {
None
} else {
@@ -161,7 +159,7 @@ impl ActivityIndicator {
.pending_work
.iter()
.map(|(token, progress)| PendingWork {
language_server_id: server_id,
language_server_name: status.name.as_str(),
progress_token: token.as_str(),
progress,
})
@@ -177,44 +175,33 @@ impl ActivityIndicator {
// Show any language server has pending activity.
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
language_server_name,
progress_token,
progress,
..
}) = pending_work.next()
{
let mut message = progress
.title
.as_deref()
.unwrap_or(progress_token)
.to_string();
let mut message = language_server_name.to_string();
message.push_str(": ");
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(progress_message);
} else {
message.push_str(progress_token);
}
if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap();
}
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(": ");
message.push_str(progress_message);
}
let additional_work_count = pending_work.count();
if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
return Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
),
icon: None,
message,
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
on_click: None,
};
}
@@ -235,11 +222,7 @@ impl ActivityIndicator {
if !downloading.is_empty() {
return Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(DOWNLOAD_ICON),
message: format!("Downloading {}...", downloading.join(", "),),
on_click: None,
};
@@ -247,11 +230,7 @@ impl ActivityIndicator {
if !checking_for_update.is_empty() {
return Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(DOWNLOAD_ICON),
message: format!(
"Checking for updates to {}...",
checking_for_update.join(", "),
@@ -262,11 +241,7 @@ impl ActivityIndicator {
if !failed.is_empty() {
return Content {
icon: Some(
Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(WARNING_ICON),
message: format!(
"Failed to download {}. Click to show error.",
failed.join(", "),
@@ -280,11 +255,7 @@ impl ActivityIndicator {
// Show any formatting failure
if let Some(failure) = self.project.read(cx).last_formatting_failure() {
return Content {
icon: Some(
Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(WARNING_ICON),
message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|_, cx| {
cx.dispatch_action(Box::new(workspace::OpenLog));
@@ -296,29 +267,17 @@ impl ActivityIndicator {
if let Some(updater) = &self.auto_updater {
return match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(DOWNLOAD_ICON),
message: "Checking for Zed updates…".to_string(),
on_click: None,
},
AutoUpdateStatus::Downloading => Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(DOWNLOAD_ICON),
message: "Downloading Zed update…".to_string(),
on_click: None,
},
AutoUpdateStatus::Installing => Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(DOWNLOAD_ICON),
message: "Installing Zed update…".to_string(),
on_click: None,
},
@@ -326,18 +285,14 @@ impl ActivityIndicator {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new({
let reload = workspace::Reload {
let restart = workspace::Restart {
binary_path: Some(binary_path.clone()),
};
move |_, cx| workspace::reload(&reload, cx)
move |_, cx| workspace::restart(&restart, cx)
})),
},
AutoUpdateStatus::Errored => Content {
icon: Some(
Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(WARNING_ICON),
message: "Auto update failed".to_string(),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&Default::default(), cx)
@@ -352,11 +307,7 @@ impl ActivityIndicator {
{
if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
return Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
icon: Some(DOWNLOAD_ICON),
message: format!("Updating {extension_id} extension…"),
on_click: None,
};
@@ -365,75 +316,6 @@ impl ActivityIndicator {
Default::default()
}
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
if self.context_menu.take().is_some() {
return;
}
self.build_lsp_work_context_menu(cx);
cx.notify();
}
fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut has_work = false;
let this = cx.view().downgrade();
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
for work in self.pending_language_server_work(cx) {
has_work = true;
let this = this.clone();
let title = SharedString::from(
work.progress
.title
.as_deref()
.unwrap_or(work.progress_token)
.to_string(),
);
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.to_string();
menu = menu.custom_entry(
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(title.clone()))
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
);
});
this.context_menu.take();
})
.ok();
},
);
} else {
menu = menu.label(title.clone());
}
}
menu
});
if has_work {
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
this.context_menu.take();
cx.notify();
})
.detach();
cx.focus_view(&context_menu);
self.context_menu = Some(context_menu);
cx.notify();
}
}
}
impl EventEmitter<Event> for ActivityIndicator {}
@@ -456,17 +338,8 @@ impl Render for ActivityIndicator {
}
result
.gap_2()
.children(content.icon)
.children(content.icon.map(|icon| svg().path(icon)))
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
.children(self.context_menu.as_ref().map(|menu| {
deferred(
anchored()
.anchor(gpui::AnchorCorner::BottomLeft)
.child(menu.clone()),
)
.with_priority(1)
}))
}
}

View File

@@ -23,7 +23,6 @@ isahc.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -4,16 +4,13 @@ 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;
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-5-sonnet", rename = "claude-3-5-sonnet-20240620")]
Claude3_5Sonnet,
#[serde(alias = "claude-3-opus", rename = "claude-3-opus-20240229")]
Claude3Opus,
#[serde(alias = "claude-3-sonnet", rename = "claude-3-sonnet-20240229")]
@@ -24,9 +21,7 @@ pub enum Model {
impl Model {
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-opus") {
if id.starts_with("claude-3-opus") {
Ok(Self::Claude3Opus)
} else if id.starts_with("claude-3-sonnet") {
Ok(Self::Claude3Sonnet)
@@ -39,7 +34,6 @@ impl Model {
pub fn id(&self) -> &'static str {
match self {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-20240620",
Model::Claude3Opus => "claude-3-opus-20240229",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-opus-20240307",
@@ -48,7 +42,6 @@ impl Model {
pub fn display_name(&self) -> &'static str {
match self {
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",

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,9 +5,6 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant.rs"
doctest = false
@@ -15,48 +12,37 @@ doctest = false
[dependencies]
anyhow.workspace = true
anthropic = { workspace = true, features = ["schemars"] }
assistant_slash_command.workspace = true
async-watch.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
feature_flags.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
paths.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
terminal_view.workspace = true
theme.workspace = true
tiktoken-rs.workspace = true
toml.workspace = true
@@ -64,7 +50,6 @@ ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
picker.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -0,0 +1,30 @@
mod current_project;
mod recent_buffers;
pub use current_project::*;
pub use recent_buffers::*;
#[derive(Default)]
pub struct AmbientContext {
pub recent_buffers: RecentBuffersContext,
pub current_project: CurrentProjectContext,
}
impl AmbientContext {
pub fn snapshot(&self) -> AmbientContextSnapshot {
AmbientContextSnapshot {
recent_buffers: self.recent_buffers.snapshot.clone(),
}
}
}
#[derive(Clone, Default, Debug)]
pub struct AmbientContextSnapshot {
pub recent_buffers: RecentBuffersSnapshot,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum ContextUpdated {
Updating,
Disabled,
}

View File

@@ -0,0 +1,178 @@
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Result};
use fs::Fs;
use gpui::{AsyncAppContext, ModelContext, Task, WeakModel};
use project::{Project, ProjectPath};
use util::ResultExt;
use crate::ambient_context::ContextUpdated;
use crate::assistant_panel::Conversation;
use crate::{LanguageModelRequestMessage, Role};
/// Ambient context about the current project.
pub struct CurrentProjectContext {
pub enabled: bool,
pub message: String,
pub pending_message: Option<Task<()>>,
}
#[allow(clippy::derivable_impls)]
impl Default for CurrentProjectContext {
fn default() -> Self {
Self {
enabled: false,
message: String::new(),
pending_message: None,
}
}
}
impl CurrentProjectContext {
/// Returns the [`CurrentProjectContext`] as a message to the language model.
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
self.enabled.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.message.clone(),
})
}
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
pub fn update(
&mut self,
fs: Arc<dyn Fs>,
project: WeakModel<Project>,
cx: &mut ModelContext<Conversation>,
) -> ContextUpdated {
if !self.enabled {
self.message.clear();
self.pending_message = None;
cx.notify();
return ContextUpdated::Disabled;
}
self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
let Some(path_to_cargo_toml) = Self::path_to_cargo_toml(project, &mut cx).log_err()
else {
return;
};
let Some(path_to_cargo_toml) = path_to_cargo_toml
.ok_or_else(|| anyhow!("no Cargo.toml"))
.log_err()
else {
return;
};
let message_task = cx
.background_executor()
.spawn(async move { Self::build_message(fs, &path_to_cargo_toml).await });
if let Some(message) = message_task.await.log_err() {
conversation
.update(&mut cx, |conversation, cx| {
conversation.ambient_context.current_project.message = message;
conversation.count_remaining_tokens(cx);
cx.notify();
})
.log_err();
}
}));
ContextUpdated::Updating
}
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: WeakModel<Project>,
cx: &mut AsyncAppContext,
) -> Result<Option<PathBuf>> {
cx.update(|cx| {
let worktree = project.update(cx, |project, _cx| {
project
.worktrees()
.next()
.ok_or_else(|| anyhow!("no worktree"))
})??;
let path_to_cargo_toml = worktree.update(cx, |worktree, _cx| {
let cargo_toml = worktree.entry_for_path("Cargo.toml")?;
Some(ProjectPath {
worktree_id: worktree.id(),
path: cargo_toml.path.clone(),
})
});
let path_to_cargo_toml = path_to_cargo_toml.and_then(|path| {
project
.update(cx, |project, cx| project.absolute_path(&path, cx))
.ok()
.flatten()
});
Ok(path_to_cargo_toml)
})?
}
}

View File

@@ -0,0 +1,145 @@
use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
use gpui::{ModelContext, Subscription, Task, WeakModel};
use language::{Buffer, BufferSnapshot, Rope};
use std::{fmt::Write, path::PathBuf, time::Duration};
use super::ContextUpdated;
pub struct RecentBuffersContext {
pub enabled: bool,
pub buffers: Vec<RecentBuffer>,
pub snapshot: RecentBuffersSnapshot,
pub pending_message: Option<Task<()>>,
}
pub struct RecentBuffer {
pub buffer: WeakModel<Buffer>,
pub _subscription: Subscription,
}
impl Default for RecentBuffersContext {
fn default() -> Self {
Self {
enabled: true,
buffers: Vec::new(),
snapshot: RecentBuffersSnapshot::default(),
pending_message: None,
}
}
}
impl RecentBuffersContext {
pub fn update(&mut self, cx: &mut ModelContext<Conversation>) -> ContextUpdated {
let source_buffers = self
.buffers
.iter()
.filter_map(|recent| {
let (full_path, snapshot) = recent
.buffer
.read_with(cx, |buffer, cx| {
(
buffer.file().map(|file| file.full_path(cx)),
buffer.snapshot(),
)
})
.ok()?;
Some(SourceBufferSnapshot {
full_path,
model: recent.buffer.clone(),
snapshot,
})
})
.collect::<Vec<_>>();
if !self.enabled || source_buffers.is_empty() {
self.snapshot.message = Default::default();
self.snapshot.source_buffers.clear();
self.pending_message = None;
cx.notify();
ContextUpdated::Disabled
} else {
self.pending_message = Some(cx.spawn(|this, mut cx| async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
let message = if source_buffers.is_empty() {
Rope::new()
} else {
cx.background_executor()
.spawn({
let source_buffers = source_buffers.clone();
async move { message_for_recent_buffers(source_buffers) }
})
.await
};
this.update(&mut cx, |this, cx| {
this.ambient_context.recent_buffers.snapshot.source_buffers = source_buffers;
this.ambient_context.recent_buffers.snapshot.message = message;
this.count_remaining_tokens(cx);
cx.notify();
})
.ok();
}));
ContextUpdated::Updating
}
}
/// Returns the [`RecentBuffersContext`] as a message to the language model.
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
self.enabled.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.snapshot.message.to_string(),
})
}
}
#[derive(Clone, Default, Debug)]
pub struct RecentBuffersSnapshot {
pub message: Rope,
pub source_buffers: Vec<SourceBufferSnapshot>,
}
#[derive(Clone)]
pub struct SourceBufferSnapshot {
pub full_path: Option<PathBuf>,
pub model: WeakModel<Buffer>,
pub snapshot: BufferSnapshot,
}
impl std::fmt::Debug for SourceBufferSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SourceBufferSnapshot")
.field("full_path", &self.full_path)
.field("model (entity id)", &self.model.entity_id())
.field("snapshot (text)", &self.snapshot.text())
.finish()
}
}
fn message_for_recent_buffers(buffers: Vec<SourceBufferSnapshot>) -> Rope {
let mut message = String::new();
writeln!(
message,
"The following is a list of recent buffers that the user has opened."
)
.unwrap();
for buffer in buffers {
if let Some(path) = buffer.full_path {
writeln!(message, "```{}", path.display()).unwrap();
} else {
writeln!(message, "```untitled").unwrap();
}
for chunk in buffer.snapshot.chunks(0..buffer.snapshot.len(), false) {
message.push_str(chunk.text);
}
if !message.ends_with('\n') {
message.push('\n');
}
message.push_str("```\n");
}
Rope::from(message.as_str())
}

View File

@@ -1,40 +1,28 @@
mod ambient_context;
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 saved_conversation;
mod search;
mod slash_command;
mod streaming_diff;
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
use assistant_slash_command::SlashCommandRegistry;
use ambient_context::AmbientContextSnapshot;
pub use assistant_panel::AssistantPanel;
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
pub(crate) use context_store::*;
use fs::Fs;
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};
pub(crate) use saved_conversation::*;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use slash_command::{
active_command, auto_command, default_command, diagnostics_command, fetch_command,
file_command, now_command, project_command, prompt_command, rustdoc_command, search_command,
tabs_command, term_command,
};
use std::{
fmt::{self, Display},
sync::Arc,
};
pub(crate) use streaming_diff::*;
actions!(
assistant,
@@ -47,10 +35,9 @@ actions!(
ResetKey,
InlineAssist,
InsertActivePrompt,
ToggleIncludeConversation,
ToggleHistory,
ApplyEdit,
ConfirmCommand,
ToggleModelSelector
ApplyEdit
]
);
@@ -89,15 +76,14 @@ 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())
}
}
@@ -106,8 +92,7 @@ impl LanguageModel {
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()),
}
}
@@ -115,8 +100,7 @@ impl LanguageModel {
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::ZedDotDev(model) => model.display_name().into(),
}
}
@@ -124,8 +108,7 @@ impl LanguageModel {
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(),
}
}
@@ -133,8 +116,7 @@ impl LanguageModel {
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,24 +161,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
| CloudModel::Claude3_5Sonnet => {
preprocess_anthropic_request(self);
}
_ => {}
},
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -223,6 +187,9 @@ pub struct LanguageModelChoiceDelta {
struct MessageMetadata {
role: Role,
status: MessageStatus,
// todo!("delete this")
#[serde(skip)]
ambient_context: AmbientContextSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -265,32 +232,11 @@ impl Assistant {
}
}
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
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(
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
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(fs.clone(), client.telemetry().clone(), cx);
RustdocStore::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
@@ -303,47 +249,13 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
cx.observe_global::<SettingsStore>(|cx| {
Assistant::update_global(cx, |assistant, cx| {
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(auto_command::AutoCommand, true);
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(term_command::TermSlashCommand, 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);
}
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),
1000..=9999 => {
let thousands = count / 1000;
let hundreds = (count % 1000 + 50) / 100;
if hundreds == 0 {
format!("{}k", thousands)
} else if hundreds == 10 {
format!("{}k", thousands + 1)
} else {
format!("{}.{}k", thousands, hundreds)
}
}
_ => format!("{}k", (count + 500) / 1000),
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,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,25 +12,21 @@ 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,
Claude3_5Sonnet,
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,
@@ -40,7 +35,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>,
@@ -48,7 +43,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")
@@ -58,10 +53,13 @@ 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),
"gpt-4o" => Ok(ZedDotDevModel::Gpt4Omni),
_ => Ok(ZedDotDevModel::Custom(value.to_owned())),
}
}
}
@@ -69,29 +67,30 @@ 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(),
"gpt-4o".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()
@@ -99,14 +98,13 @@ 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::Claude3_5Sonnet => "claude-3-5-sonnet",
Self::Claude3Opus => "claude-3-opus",
Self::Claude3Sonnet => "claude-3-sonnet",
Self::Claude3Haiku => "claude-3-haiku",
@@ -120,7 +118,6 @@ impl CloudModel {
Self::Gpt4 => "GPT 4",
Self::Gpt4Turbo => "GPT 4 Turbo",
Self::Gpt4Omni => "GPT 4 Omni",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
@@ -133,22 +130,10 @@ impl CloudModel {
Self::Gpt3Point5Turbo => 2048,
Self::Gpt4 => 4096,
Self::Gpt4Turbo | Self::Gpt4Omni => 128000,
Self::Claude3_5Sonnet
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => 200000,
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)]
@@ -160,64 +145,51 @@ 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,
#[serde(default)]
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "anthropic")]
Anthropic {
model: AnthropicModel,
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
},
Ollama {
model: OllamaModel,
#[serde(default)]
default_model: AnthropicModel,
#[serde(default = "anthropic_api_url")]
api_url: String,
#[serde(default)]
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 {
open_ai::OPEN_AI_API_URL.to_string()
}
#[derive(Debug, Default)]
fn anthropic_api_url() -> String {
anthropic::ANTHROPIC_API_URL.to_string()
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct AssistantSettings {
pub enabled: bool,
pub button: bool,
@@ -268,16 +240,16 @@ 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()),
Some(AssistantProvider::OpenAi {
default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
api_url: open_ai_api_url.clone(),
low_speed_timeout_in_seconds: None,
})
} else {
settings.default_open_ai_model.clone().map(|open_ai_model| {
AssistantProviderContent::OpenAi {
default_model: Some(open_ai_model),
api_url: None,
AssistantProvider::OpenAi {
default_model: open_ai_model,
api_url: open_ai_url(),
low_speed_timeout_in_seconds: None,
}
})
@@ -298,71 +270,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)]
@@ -411,7 +318,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)]
@@ -432,11 +339,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>,
@@ -469,112 +376,31 @@ 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;
*low_speed_timeout_in_seconds = low_speed_timeout_in_seconds_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;
}
}
}
@@ -584,7 +410,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;
}
@@ -607,8 +433,8 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
model: OpenAiModel::FourOmni,
api_url: open_ai::OPEN_AI_API_URL.into(),
default_model: OpenAiModel::FourOmni,
api_url: open_ai_url(),
low_speed_timeout_in_seconds: None,
}
);
@@ -629,7 +455,7 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
model: OpenAiModel::FourOmni,
default_model: OpenAiModel::FourOmni,
api_url: "test-url".into(),
low_speed_timeout_in_seconds: None,
}
@@ -649,8 +475,8 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
model: OpenAiModel::Four,
api_url: open_ai::OPEN_AI_API_URL.into(),
default_model: OpenAiModel::Four,
api_url: open_ai_url(),
low_speed_timeout_in_seconds: None,
}
);
@@ -675,7 +501,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,696 @@
use crate::{
streaming_diff::{Hunk, StreamingDiff},
CompletionProvider, LanguageModelRequest,
};
use anyhow::Result;
use client::telemetry::Telemetry;
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 multi_buffer::MultiBufferRow;
use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
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,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
}
impl EventEmitter<Event> for Codegen {}
impl Codegen {
pub fn new(
buffer: Model<MultiBuffer>,
kind: CodegenKind,
telemetry: Option<Arc<Telemetry>>,
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(()),
telemetry,
_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(MultiBufferRow(selection_start.row)));
let model_telemetry_id = prompt.model.telemetry_id();
let response = CompletionProvider::global(cx).complete(prompt);
let telemetry = self.telemetry.clone();
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 mut response_latency = None;
let request_start = Instant::now();
let diff = async {
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 {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());
}
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(())
};
let error_message = diff.await.err().map(|error| error.to_string());
if let Some(telemetry) = telemetry {
telemetry.report_assistant_event(
None,
telemetry_events::AssistantKind::Inline,
model_telemetry_id,
response_latency,
error_message,
);
}
});
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 }, None, 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 }, None, 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 }, None, 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,14 @@
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},
@@ -27,43 +25,36 @@ 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,
default_model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
model.clone(),
default_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,13 +65,13 @@ 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(),
default_model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
@@ -89,42 +80,27 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
CompletionProvider::Anthropic(provider),
AssistantProvider::Anthropic {
model,
default_model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
model.clone(),
default_model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
);
}
(
CompletionProvider::Ollama(provider),
AssistantProvider::Ollama {
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,
cx,
);
provider.update(default_model.clone(), settings_version);
}
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
provider.update(model.clone(), settings_version);
}
(_, AssistantProvider::ZedDotDev { model }) => {
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
model.clone(),
(_, AssistantProvider::ZedDotDev { default_model }) => {
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
default_model.clone(),
client.clone(),
settings_version,
cx,
@@ -133,13 +109,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
_,
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),
@@ -149,36 +125,19 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
_,
AssistantProvider::Anthropic {
model,
default_model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
model.clone(),
default_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,
));
}
}
})
})
@@ -188,10 +147,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 +159,11 @@ 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!(),
}
@@ -239,8 +173,7 @@ impl CompletionProvider {
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,
}
@@ -250,8 +183,7 @@ impl CompletionProvider {
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(())),
}
@@ -261,8 +193,7 @@ impl CompletionProvider {
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!(),
}
@@ -272,21 +203,23 @@ impl CompletionProvider {
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::Anthropic(provider) => {
LanguageModel::Anthropic(provider.default_model())
}
CompletionProvider::ZedDotDev(provider) => {
LanguageModel::ZedDotDev(provider.default_model())
}
#[cfg(test)]
CompletionProvider::Fake(_) => LanguageModel::default(),
CompletionProvider::Fake(_) => unimplemented!(),
}
}
@@ -298,10 +231,9 @@ impl CompletionProvider {
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!(),
}
}
@@ -312,8 +244,7 @@ impl CompletionProvider {
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,18 +1,17 @@
use crate::count_open_ai_tokens;
use crate::{
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
Role,
};
use crate::{count_open_ai_tokens, LanguageModelRequestMessage};
use anthropic::{stream_completion, Request, RequestMessage};
use anthropic::{stream_completion, Request, RequestMessage, Role as AnthropicRole};
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 gpui::{AnyView, AppContext, FontStyle, FontWeight, 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;
@@ -20,7 +19,7 @@ use util::ResultExt;
pub struct AnthropicCompletionProvider {
api_key: Option<String>,
api_url: String,
model: AnthropicModel,
default_model: AnthropicModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
@@ -28,7 +27,7 @@ pub struct AnthropicCompletionProvider {
impl AnthropicCompletionProvider {
pub fn new(
model: AnthropicModel,
default_model: AnthropicModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
@@ -37,7 +36,7 @@ impl AnthropicCompletionProvider {
Self {
api_key: None,
api_url,
model,
default_model,
http_client,
low_speed_timeout,
settings_version,
@@ -46,21 +45,17 @@ impl AnthropicCompletionProvider {
pub fn update(
&mut self,
model: AnthropicModel,
default_model: AnthropicModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.model = model;
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 = AnthropicModel> {
AnthropicModel::iter()
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
@@ -110,8 +105,8 @@ impl AnthropicCompletionProvider {
.into()
}
pub fn model(&self) -> AnthropicModel {
self.model.clone()
pub fn default_model(&self) -> AnthropicModel {
self.default_model.clone()
}
pub fn count_tokens(
@@ -167,37 +162,53 @@ impl AnthropicCompletionProvider {
.boxed()
}
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
preprocess_anthropic_request(&mut request);
fn to_anthropic_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::Anthropic(model) => model,
_ => self.model(),
_ => self.default_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;
let mut messages: Vec<RequestMessage> = Vec::new();
for message in request.messages {
if message.content.is_empty() {
continue;
}
match message.role {
Role::User | Role::Assistant => {
let role = match message.role {
Role::User => AnthropicRole::User,
Role::Assistant => AnthropicRole::Assistant,
_ => unreachable!(),
};
if let Some(last_message) = messages.last_mut() {
if last_message.role == role {
last_message.content.push_str("\n\n");
last_message.content.push_str(&message.content);
continue;
}
}
messages.push(RequestMessage {
role,
content: message.content,
});
}
Role::System => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.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(),
messages,
stream: true,
system: system_message,
max_tokens: 4092,
@@ -205,49 +216,6 @@ impl AnthropicCompletionProvider {
}
}
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,
@@ -293,7 +261,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,
@@ -349,7 +317,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,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,17 +1,16 @@
use crate::assistant_settings::CloudModel;
use crate::assistant_settings::ZedDotDevModel;
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 gpui::{AnyView, AppContext, FontStyle, FontWeight, Task, TextStyle, View, WhiteSpace};
use http::HttpClient;
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;
@@ -19,7 +18,7 @@ use util::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,
@@ -27,7 +26,7 @@ pub struct OpenAiCompletionProvider {
impl OpenAiCompletionProvider {
pub fn new(
model: OpenAiModel,
default_model: OpenAiModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
@@ -36,7 +35,7 @@ impl OpenAiCompletionProvider {
Self {
api_key: None,
api_url,
model,
default_model,
http_client,
low_speed_timeout,
settings_version,
@@ -45,21 +44,17 @@ impl OpenAiCompletionProvider {
pub fn update(
&mut self,
model: OpenAiModel,
default_model: OpenAiModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.model = model;
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 +104,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(
@@ -157,7 +152,7 @@ impl OpenAiCompletionProvider {
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::OpenAi(model) => model,
_ => self.model(),
_ => self.default_model(),
};
Request {
@@ -210,10 +205,9 @@ pub fn count_open_ai_tokens(
match request.model {
LanguageModel::Anthropic(_)
| LanguageModel::Cloud(CloudModel::Claude3_5Sonnet)
| LanguageModel::Cloud(CloudModel::Claude3Opus)
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
| LanguageModel::ZedDotDev(ZedDotDevModel::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)
@@ -279,7 +273,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,
@@ -337,7 +331,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,22 +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::ZedDotDev(ZedDotDevModel::Gpt4)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Turbo)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Omni)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt3Point5Turbo) => {
count_open_ai_tokens(request, cx.background_executor())
}
LanguageModel::Cloud(
CloudModel::Claude3_5Sonnet
| 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
@@ -130,10 +113,8 @@ impl CloudCompletionProvider {
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,225 +0,0 @@
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, Model, ModelContext, Task};
use paths::contexts_dir;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
use ui::Context;
use util::{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,
pub slash_command_output_sections: Vec<SlashCommandOutputSection<usize>>,
}
impl SavedContext {
pub const VERSION: &'static str = "0.3.0";
}
#[derive(Serialize, Deserialize)]
pub struct SavedContextV0_2_0 {
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,
}
#[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.2.0" => {
let saved_context =
serde_json::from_value::<SavedContextV0_2_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,
slash_command_output_sections: Vec::new(),
})
}
"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,
slash_command_output_sections: Vec::new(),
})
}
_ => 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();
})
})
}
}

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::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, 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 {
PopoverMenu::new("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

@@ -33,32 +33,35 @@ pub fn generate_content_prompt(
)?;
}
writeln!(
prompt,
"The user has the following file open in the editor:"
)?;
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
write!(prompt, "```")?;
if let Some(language_name) = language_name {
write!(prompt, "{language_name}")?;
}
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.as_rope().chunks_in_range(0..range.start) {
prompt.push_str(chunk);
}
prompt.push_str("<|CURSOR|>");
for chunk in buffer.as_rope().chunks_in_range(range.start..buffer.len()) {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|CURSOR|>` span is."
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
@@ -72,42 +75,11 @@ pub fn generate_content_prompt(
)
.unwrap();
} else {
write!(prompt, "```")?;
for chunk in buffer.as_rope().chunks() {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"In particular, the following piece of text is selected:"
)?;
write!(prompt, "```")?;
if let Some(language_name) = language_name {
write!(prompt, "{language_name}")?;
}
prompt.push('\n');
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
writeln!(
prompt,
"Modify the user's selected {content_type} based upon the users prompt: {user_prompt}"
)
.unwrap();
writeln!(
prompt,
"You must reply with only the adjusted {content_type}, not the entire file."
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}

View File

@@ -0,0 +1,126 @@
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)
{
// This is used to filter out conversations saved by the new assistant.
if !re.is_match(file_name) {
continue;
}
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}

View File

@@ -1,368 +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::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use rope::Point;
use std::{
ops::Range,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use ui::ActiveTheme;
use workspace::Workspace;
pub mod active_command;
pub mod auto_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 mod term_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
}
}
pub fn create_label_for_command(
command_name: &str,
arguments: &[&str],
cx: &AppContext,
) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}

View File

@@ -1,101 +0,0 @@
use super::{
diagnostics_command::write_single_file_diagnostics,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::{anyhow, Result};
use editor::Editor;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::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: Arc<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 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 task = cx.background_executor().spawn({
let path = path.clone();
async move {
let mut output = String::new();
output.push_str(&codeblock_fence_for_path(path.as_deref(), None));
for chunk in snapshot.as_rope().chunks() {
output.push_str(chunk);
}
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```\n");
let has_diagnostics =
write_single_file_diagnostics(&mut output, path.as_deref(), &snapshot);
if output.ends_with('\n') {
output.pop();
}
(output, has_diagnostics)
}
});
cx.foreground_executor().spawn(async move {
let (text, has_diagnostics) = task.await;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![build_entry_output_section(
range,
path.as_deref(),
false,
None,
)],
run_commands_in_text: has_diagnostics,
})
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -1,193 +0,0 @@
use super::create_label_for_command;
use super::{SlashCommand, SlashCommandOutput};
use crate::{CompletionProvider, LanguageModelRequest, LanguageModelRequestMessage, Role};
use anyhow::{anyhow, Result};
use futures::StreamExt;
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, LspAdapterDelegate};
use std::sync::{atomic::AtomicBool, Arc};
use ui::WindowContext;
use workspace::Workspace;
pub(crate) struct AutoCommand;
impl SlashCommand for AutoCommand {
fn name(&self) -> String {
"auto".into()
}
fn description(&self) -> String {
"Automatically infer what context to add, based on your prompt".into()
}
fn menu_text(&self) -> String {
"Automatically Infer Context".into()
}
fn label(&self, cx: &AppContext) -> CodeLabel {
create_label_for_command("auto", &["--prompt"], cx)
}
fn complete_argument(
self: Arc<Self>,
_query: String,
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
// There's no autocomplete for a prompt, since it's arbitrary text.
Task::ready(Ok(Vec::new()))
}
fn requires_argument(&self) -> bool {
true
}
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 prompt")));
};
let prompt = format!("{PROMPT_INSTRUCTIONS_BEFORE_SUMMARY}\n{SUMMARY}\n{PROMPT_INSTRUCTIONS_AFTER_SUMMARY}\n{argument}");
let request = LanguageModelRequest {
model: CompletionProvider::global(cx).model(),
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: prompt,
}],
stop: vec![],
temperature: 1.0,
};
let stream = CompletionProvider::global(cx).complete(request);
let mut wip_action: String = String::new();
let task: Task<Result<String>> = cx.spawn(|_cx| async move {
let mut actions_text = String::new();
let stream_completion = async {
let mut messages = stream.await?;
while let Some(message) = messages.next().await {
let text = message?;
chunked_line(&mut wip_action, &text, |line| {
actions_text.push('/');
actions_text.push_str(line);
actions_text.push('\n');
});
smol::future::yield_now().await;
}
anyhow::Ok(())
};
stream_completion.await?;
Ok(actions_text)
});
// As a convenience, append /auto's argument to the end of the prompt so you don't have to write it again.
let argument = argument.to_string();
cx.background_executor().spawn(async move {
let mut text = task.await?;
text.push_str(&argument);
Ok(SlashCommandOutput {
text,
sections: Vec::new(),
run_commands_in_text: true,
})
})
}
}
const PROMPT_INSTRUCTIONS_BEFORE_SUMMARY: &str = r#"
I'm going to give you a prompt. I don't want you to respond
to the prompt itself. I want you to figure out which of the following
actions on my project, if any, would help you answer the prompt.
Here are the actions:
## file
This action's parameter is a file path to one of the files
in the project. If you ask for this action, I will tell you
the full contents of the file, so you can learn all the
details of the file.
## search
This action's parameter is a string to search for across
the project. It will tell you which files this string
(or similar strings; it is a semantic search) appear in,
as well as some context of the lines surrounding each result.
---
That was the end of the list of actions.
Here is an XML summary of each of the files in my project:
"#;
const PROMPT_INSTRUCTIONS_AFTER_SUMMARY: &str = r#"
Actions have a cost, so only include actions that you think
will be helpful to you in doing a great job answering the
prompt in the future.
You must respond ONLY with a list of actions you would like to
perform. Each action should be on its own line, and followed by a space and then its parameter.
Actions can be performed more than once with different parameters.
Here is an example valid response:
```
file path/to/my/file.txt
file path/to/another/file.txt
search something to search for
search something else to search for
```
Once again, do not forget: you must respond ONLY in the format of
one action per line, and the action name should be followed by
its parameter. Your response must not include anything other
than a list of actions, with one action per line, in this format.
It is extremely important that you do not deviate from this format even slightly!
This is the end of my instructions for how to respond. The rest is the prompt:
"#;
const SUMMARY: &str = "";
fn chunked_line(wip: &mut String, chunk: &str, mut on_line_end: impl FnMut(&str)) {
// The first iteration of the loop should just push to wip
// and nothing else. We only push what we encountered in
// previous iterations of the loop.
//
// This correctly handles both the scenario where no
// newlines are encountered (the loop will only run once,
// and so will only push to wip), as well as the scenario
// where the chunk contains at least one newline but
// does not end in a newline (the last iteration of the
// loop will update wip but will not run anything).
let mut is_first_iteration = true;
for line in chunk.split('\n') {
if is_first_iteration {
is_first_iteration = false;
} else {
// Since this isn't the first iteration of the loop, we definitely hit a newline
// at the end of the previous iteration! Run the function on whatever wip we have.
on_line_end(wip);
wip.clear();
}
wip.push_str(line);
}
}

View File

@@ -1,79 +0,0 @@
use super::{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: Arc<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();
text.push('\n');
for prompt in prompts {
if let Some(title) = prompt.title {
writeln!(text, "/prompt {}", title).unwrap();
}
}
text.pop();
if text.is_empty() {
text.push('\n');
}
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..text.len(),
icon: IconName::Library,
label: "Default".into(),
}],
text,
run_commands_in_text: true,
})
})
}
}

View File

@@ -1,490 +0,0 @@
use super::{create_label_for_command, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fuzzy::{PathMatch, StringMatchCandidate};
use gpui::{AppContext, Model, 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::path::{Path, PathBuf};
use std::{
ops::Range,
sync::{atomic::AtomicBool, Arc},
};
use ui::prelude::*;
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: true,
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 label(&self, cx: &AppContext) -> language::CodeLabel {
create_label_for_command("diagnostics", &[INCLUDE_WARNINGS_ARGUMENT], cx)
}
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: Arc<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 Some((text, sections)) = task.await? else {
return Ok(SlashCommandOutput::default());
};
Ok(SlashCommandOutput {
text,
sections: sections
.into_iter()
.map(|(range, placeholder_type)| SlashCommandOutputSection {
range,
icon: match placeholder_type {
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
PlaceholderType::File(_) => IconName::File,
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => {
IconName::XCircle
}
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
IconName::ExclamationTriangle
}
},
label: match placeholder_type {
PlaceholderType::Root(summary, source) => {
let mut label = String::new();
label.push_str("Diagnostics");
if let Some(source) = source {
write!(label, " ({})", source).unwrap();
}
if summary.error_count > 0 || summary.warning_count > 0 {
label.push(':');
if summary.error_count > 0 {
write!(label, " {} errors", summary.error_count).unwrap();
if summary.warning_count > 0 {
label.push_str(",");
}
}
if summary.warning_count > 0 {
write!(label, " {} warnings", summary.warning_count)
.unwrap();
}
}
label.into()
}
PlaceholderType::File(file_path) => file_path.into(),
PlaceholderType::Diagnostic(_, message) => message.into(),
},
})
.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 {
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.to_owned()]).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<Option<(String, Vec<(Range<usize>, PlaceholderType)>)>>> {
let error_source = if let Some(path_matcher) = &options.path_matcher {
debug_assert_eq!(path_matcher.sources().len(), 1);
Some(path_matcher.sources().first().cloned().unwrap_or_default())
} else {
None
};
let glob_is_exact_file_match = if let Some(path) = options
.path_matcher
.as_ref()
.and_then(|pm| pm.sources().first())
{
PathBuf::try_from(path)
.ok()
.and_then(|path| {
project.read(cx).worktrees().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()
})
})
.is_some()
} else {
false
};
let project_handle = project.downgrade();
let diagnostic_summaries: Vec<_> = project
.read(cx)
.diagnostic_summaries(false, cx)
.flat_map(|(path, _, summary)| {
let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
let mut path_buf = PathBuf::from(worktree.read(cx).root_name());
path_buf.push(&path.path);
Some((path, path_buf, summary))
})
.collect();
cx.spawn(|mut cx| async move {
let mut text = String::new();
if let Some(error_source) = error_source.as_ref() {
writeln!(text, "diagnostics: {}", error_source).unwrap();
} else {
writeln!(text, "diagnostics").unwrap();
}
let mut sections: Vec<(Range<usize>, PlaceholderType)> = Vec::new();
let mut project_summary = DiagnosticSummary::default();
for (project_path, path, summary) in diagnostic_summaries {
if let Some(path_matcher) = &options.path_matcher {
if !path_matcher.is_match(&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 = path.to_string_lossy().to_string();
if !glob_is_exact_file_match {
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,
);
}
if !glob_is_exact_file_match {
sections.push((
last_end..text.len().saturating_sub(1),
PlaceholderType::File(file_path),
))
}
}
// No diagnostics found
if sections.is_empty() {
return Ok(None);
}
sections.push((
0..text.len(),
PlaceholderType::Root(project_summary, error_source),
));
Ok(Some((text, sections)))
})
}
pub fn buffer_has_error_diagnostics(snapshot: &BufferSnapshot) -> bool {
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
if entry.diagnostic.severity == DiagnosticSeverity::ERROR {
return true;
}
}
false
}
pub fn write_single_file_diagnostics(
output: &mut String,
path: Option<&Path>,
snapshot: &BufferSnapshot,
) -> bool {
if let Some(path) = path {
if buffer_has_error_diagnostics(&snapshot) {
output.push_str("/diagnostics ");
output.push_str(&path.to_string_lossy());
return true;
}
}
false
}
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, Option<String>),
File(String),
Diagnostic(DiagnosticType, String),
}
#[derive(Copy, Clone)]
pub enum DiagnosticType {
Warning,
Error,
}
impl DiagnosticType {
pub fn as_str(&self) -> &'static str {
match self {
DiagnosticType::Warning => "warning",
DiagnosticType::Error => "error",
}
}
}

View File

@@ -1,163 +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::*;
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::WebpageChromeRemover)),
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: Arc<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,
icon: IconName::AtSign,
label: format!("fetch {}", url).into(),
}],
run_commands_in_text: false,
})
})
}
}

View File

@@ -1,365 +0,0 @@
use super::{diagnostics_command::write_single_file_diagnostics, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task, View, WeakView};
use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
use project::{PathMatchCandidateSet, Project};
use std::{
fmt::Write,
ops::Range,
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Arc},
};
use ui::prelude::*;
use util::{paths::PathMatcher, ResultExt};
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::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 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: Arc<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 task = collect_files(workspace.read(cx).project().clone(), argument, cx);
cx.foreground_executor().spawn(async move {
let (text, ranges) = task.await?;
Ok(SlashCommandOutput {
text,
sections: ranges
.into_iter()
.map(|(range, path, entry_type)| {
build_entry_output_section(
range,
Some(&path),
entry_type == EntryType::Directory,
None,
)
})
.collect(),
run_commands_in_text: true,
})
})
}
}
#[derive(Clone, Copy, PartialEq)]
enum EntryType {
File,
Directory,
}
fn collect_files(
project: Model<Project>,
glob_input: &str,
cx: &mut AppContext,
) -> Task<Result<(String, Vec<(Range<usize>, PathBuf, EntryType)>)>> {
let Ok(matcher) = PathMatcher::new(&[glob_input.to_owned()]) else {
return Task::ready(Err(anyhow!("invalid path")));
};
let project_handle = project.downgrade();
let snapshots = project
.read(cx)
.worktrees()
.map(|worktree| worktree.read(cx).snapshot())
.collect::<Vec<_>>();
cx.spawn(|mut cx| async move {
let mut text = String::new();
let mut ranges = Vec::new();
for snapshot in snapshots {
let worktree_id = snapshot.id();
let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
let mut folded_directory_names_stack = Vec::new();
let mut is_top_level_directory = true;
for entry in snapshot.entries(false, 0) {
let mut path_including_worktree_name = PathBuf::new();
path_including_worktree_name.push(snapshot.root_name());
path_including_worktree_name.push(&entry.path);
if !matcher.is_match(&path_including_worktree_name) {
continue;
}
while let Some((dir, _, _)) = directory_stack.last() {
if entry.path.starts_with(dir) {
break;
}
let (_, entry_name, start) = directory_stack.pop().unwrap();
ranges.push((
start..text.len().saturating_sub(1),
PathBuf::from(entry_name),
EntryType::Directory,
));
}
let filename = entry
.path
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string();
if entry.is_dir() {
// Auto-fold directories that contain no files
let mut child_entries = snapshot.child_entries(&entry.path);
if let Some(child) = child_entries.next() {
if child_entries.next().is_none() && child.kind.is_dir() {
if is_top_level_directory {
is_top_level_directory = false;
folded_directory_names_stack.push(
path_including_worktree_name.to_string_lossy().to_string(),
);
} else {
folded_directory_names_stack.push(filename.to_string());
}
continue;
}
} else {
// Skip empty directories
folded_directory_names_stack.clear();
continue;
}
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
let entry_start = text.len();
if prefix_paths.is_empty() {
if is_top_level_directory {
text.push_str(&path_including_worktree_name.to_string_lossy());
is_top_level_directory = false;
} else {
text.push_str(&filename);
}
directory_stack.push((entry.path.clone(), filename, entry_start));
} else {
let entry_name = format!("{}/{}", prefix_paths, &filename);
text.push_str(&entry_name);
directory_stack.push((entry.path.clone(), entry_name, entry_start));
}
text.push('\n');
} else if entry.is_file() {
let Some(open_buffer_task) = project_handle
.update(&mut cx, |project, cx| {
project.open_buffer((worktree_id, &entry.path), cx)
})
.ok()
else {
continue;
};
if let Some(buffer) = open_buffer_task.await.log_err() {
let snapshot = cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
let prev_len = text.len();
collect_file_content(&mut text, &snapshot, filename.clone());
text.push('\n');
if !write_single_file_diagnostics(
&mut text,
Some(&path_including_worktree_name),
&snapshot,
) {
text.pop();
}
ranges.push((
prev_len..text.len(),
PathBuf::from(filename),
EntryType::File,
));
text.push('\n');
}
}
}
while let Some((dir, _, start)) = directory_stack.pop() {
let mut root_path = PathBuf::new();
root_path.push(snapshot.root_name());
root_path.push(&dir);
ranges.push((start..text.len(), root_path, EntryType::Directory));
}
}
Ok((text, ranges))
})
}
fn collect_file_content(buffer: &mut String, snapshot: &BufferSnapshot, filename: String) {
let mut content = snapshot.text();
LineEnding::normalize(&mut content);
buffer.reserve(filename.len() + content.len() + 9);
buffer.push_str(&codeblock_fence_for_path(
Some(&PathBuf::from(filename)),
None,
));
buffer.push_str(&content);
if !buffer.ends_with('\n') {
buffer.push('\n');
}
buffer.push_str("```");
}
pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<Range<u32>>) -> String {
let mut text = String::new();
write!(text, "```").unwrap();
if let Some(path) = path {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
write!(text, "{} ", extension).unwrap();
}
write!(text, "{}", path.display()).unwrap();
} else {
write!(text, "untitled").unwrap();
}
if let Some(row_range) = row_range {
write!(text, ":{}-{}", row_range.start + 1, row_range.end + 1).unwrap();
}
text.push('\n');
text
}
pub fn build_entry_output_section(
range: Range<usize>,
path: Option<&Path>,
is_directory: bool,
line_range: Option<Range<u32>>,
) -> SlashCommandOutputSection<usize> {
let mut label = if let Some(path) = path {
path.to_string_lossy().to_string()
} else {
"untitled".to_string()
};
if let Some(line_range) = line_range {
write!(label, ":{}-{}", line_range.start, line_range.end).unwrap();
}
let icon = if is_directory {
IconName::Folder
} else {
IconName::File
};
SlashCommandOutputSection {
range,
icon,
label: label.into(),
}
}

View File

@@ -1,82 +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: Arc<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,
icon: IconName::CountdownTimer,
label: now.to_rfc3339().into(),
}],
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,150 +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::*;
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: Arc<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,
icon: IconName::FileTree,
label: "Project".into(),
}],
run_commands_in_text: false,
})
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -1,88 +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::*;
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: Arc<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 mut prompt = prompt.await?;
if prompt.is_empty() {
prompt.push('\n');
}
let range = 0..prompt.len();
Ok(SlashCommandOutput {
text: prompt,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::Library,
label: title,
}],
run_commands_in_text: true,
})
})
}
}

View File

@@ -1,238 +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, CrateName, LocalProvider, RustdocSource, RustdocStore};
use ui::prelude::*;
use util::{maybe, ResultExt};
use workspace::Workspace;
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: Arc<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(),
if item_path.is_empty() {
None
} else {
Some(item_path.join("::"))
},
)
.await;
if let Ok(item_docs) = item_docs {
anyhow::Ok((RustdocSource::Index, 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();
let crate_path = module_path
.map(|module_path| format!("{}::{}", crate_name, module_path))
.unwrap_or_else(|| crate_name.to_string());
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::FileRust,
label: format!(
"rustdoc ({source}): {crate_path}",
source = match source {
RustdocSource::Index => "index",
RustdocSource::Local => "local",
RustdocSource::DocsDotRs => "docs.rs",
}
)
.into(),
}],
run_commands_in_text: false,
})
})
}
}

View File

@@ -1,174 +0,0 @@
use super::{
create_label_for_command,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::Result;
use assistant_slash_command::SlashCommandOutputSection;
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, LineEnding, LspAdapterDelegate};
use semantic_index::SemanticIndex;
use std::{
fmt::Write,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, 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 {
create_label_for_command("search", &["--n"], cx)
}
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: Arc<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_row = file_content[0..range_start].matches('\n').count() as u32;
let end_row = file_content[0..range_end].matches('\n').count() as u32;
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();
text.push_str(&codeblock_fence_for_path(
Some(&result.path),
Some(start_row..end_row),
));
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(build_entry_output_section(
section_start_ix..section_end_ix,
Some(&full_path),
false,
Some(start_row + 1..end_row + 1),
));
}
let query = SharedString::from(query);
sections.push(SlashCommandOutputSection {
range: 0..text.len(),
icon: IconName::MagnifyingGlass,
label: query,
});
SlashCommandOutput {
text,
sections,
run_commands_in_text: false,
}
})
.await;
Ok(output)
})
}
}

View File

@@ -1,117 +0,0 @@
use super::{
diagnostics_command::write_single_file_diagnostics,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::{anyhow, Result};
use collections::HashMap;
use editor::Editor;
use gpui::{AppContext, Entity, Task, WeakView};
use language::LspAdapterDelegate;
use std::{fmt::Write, sync::Arc};
use ui::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: Arc<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();
let mut has_diagnostics = false;
for (full_path, buffer, _) in open_buffers {
let section_start_ix = text.len();
text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
for chunk in buffer.as_rope().chunks() {
text.push_str(chunk);
}
if !text.ends_with('\n') {
text.push('\n');
}
writeln!(text, "```").unwrap();
if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
has_diagnostics = true;
}
if !text.ends_with('\n') {
text.push('\n');
}
let section_end_ix = text.len() - 1;
sections.push(build_entry_output_section(
section_start_ix..section_end_ix,
full_path.as_deref(),
false,
None,
));
}
Ok(SlashCommandOutput {
text,
sections,
run_commands_in_text: has_diagnostics,
})
}),
Err(error) => Task::ready(Err(error)),
}
}
}

View File

@@ -1,105 +0,0 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, LspAdapterDelegate};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::prelude::*;
use workspace::Workspace;
use super::create_label_for_command;
pub(crate) struct TermSlashCommand;
const LINE_COUNT_ARG: &str = "--line-count";
impl SlashCommand for TermSlashCommand {
fn name(&self) -> String {
"term".into()
}
fn label(&self, cx: &AppContext) -> CodeLabel {
create_label_for_command("term", &[LINE_COUNT_ARG], cx)
}
fn description(&self) -> String {
"insert terminal output".into()
}
fn menu_text(&self) -> String {
"Insert terminal output".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
self: Arc<Self>,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(vec![LINE_COUNT_ARG.to_string()]))
}
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(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Err(anyhow::anyhow!("no terminal panel open")));
};
let Some(active_terminal) = terminal_panel
.read(cx)
.pane()
.read(cx)
.active_item()
.and_then(|t| t.downcast::<TerminalView>())
else {
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
};
let line_count = argument.and_then(|a| parse_argument(a)).unwrap_or(20);
let lines = active_terminal
.read(cx)
.model()
.read(cx)
.last_n_non_empty_lines(line_count);
let mut text = String::new();
text.push_str("Terminal output:\n");
text.push_str(&lines.join("\n"));
let range = 0..text.len();
Task::ready(Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::Terminal,
label: "Terminal".into(),
}],
run_commands_in_text: false,
}))
}
}
fn parse_argument(argument: &str) -> Option<usize> {
let mut args = argument.split(' ');
if args.next() == Some(LINE_COUNT_ARG) {
if let Some(line_count) = args.next().and_then(|s| s.parse::<usize>().ok()) {
return Some(line_count);
}
}
None
}

View File

@@ -1,5 +1,5 @@
[package]
name = "repl"
name = "assistant2"
version = "0.1.0"
edition = "2021"
publish = false
@@ -9,41 +9,58 @@ license = "GPL-3.0-or-later"
workspace = true
[lib]
path = "src/repl.rs"
doctest = false
path = "src/assistant2.rs"
[features]
default = []
stories = ["dep:story"]
[dependencies]
anyhow.workspace = true
alacritty_terminal.workspace = true
async-dispatcher.workspace = true
base64.workspace = true
assistant_tooling.workspace = true
client.workspace = true
chrono.workspace = true
collections.workspace = true
editor.workspace = true
gpui.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
image.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
markdown.workspace = true
open_ai.workspace = true
picker.workspace = true
project.workspace = true
runtimelib.workspace = true
regex.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true
terminal_view.workspace = true
ui.workspace = true
uuid.workspace = true
util.workspace = true
unindent.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
markdown = { workspace = true, features = ["test-support"] }
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"] }
http = { 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 defined in my project using the following query: `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,3 @@
Use tools frequently, especially when referring to files and code. The Zed editor we're working in can show me files directly when you add annotations. Be concise in chat, bountiful in tool calling.
Teach me everything you can about how zed loads settings. Please annotate the code inline.

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.

File diff suppressed because it is too large Load Diff

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,3 @@
mod active_file;
pub use active_file::*;

View File

@@ -0,0 +1,144 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{anyhow, Result};
use assistant_tooling::{AttachmentOutput, LanguageModelAttachment, ProjectContext};
use editor::Editor;
use gpui::{Render, Task, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectPath;
use serde::{Deserialize, Serialize};
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
use util::maybe;
use workspace::Workspace;
#[derive(Serialize, Deserialize)]
pub struct ActiveEditorAttachment {
#[serde(skip)]
buffer: Option<WeakModel<Buffer>>,
path: Option<PathBuf>,
}
pub struct FileAttachmentView {
project_path: Option<ProjectPath>,
buffer: Option<WeakModel<Buffer>>,
error: Option<anyhow::Error>,
}
impl Render for FileAttachmentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(error) = &self.error {
return div().child(error.to_string()).into_any_element();
}
let filename: SharedString = self
.project_path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip(move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx))
.into_any_element()
}
}
impl AttachmentOutput for FileAttachmentView {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
if let Some(path) = &self.project_path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
}
if let Some(buffer) = self.buffer.as_ref().and_then(|buffer| buffer.upgrade()) {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
String::new()
}
}
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 name(&self) -> Arc<str> {
"active-editor-attachment".into()
}
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())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok());
return Ok(ActiveEditorAttachment {
buffer: Some(buffer.downgrade()),
path,
});
} else {
Err(anyhow!("no active buffer"))
}
}))
}
fn view(
&self,
output: Result<ActiveEditorAttachment>,
cx: &mut WindowContext,
) -> View<Self::View> {
let error;
let project_path;
let buffer;
match output {
Ok(output) => {
error = None;
let workspace = self.workspace.upgrade().unwrap();
let project = workspace.read(cx).project();
project_path = output
.path
.and_then(|path| project.read(cx).project_path_for_absolute_path(&path, cx));
buffer = output.buffer;
}
Err(err) => {
error = Some(err);
buffer = None;
project_path = None;
}
}
cx.new_view(|_cx| FileAttachmentView {
project_path,
buffer,
error,
})
}
}

View File

@@ -0,0 +1,179 @@
use anyhow::Result;
use assistant_tooling::ToolFunctionDefinition;
use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::Global;
use std::sync::Arc;
pub use open_ai::RequestMessage as CompletionMessage;
#[derive(Clone)]
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
impl 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,90 @@
use std::cmp::Reverse;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment};
use fs::Fs;
use futures::StreamExt;
use gpui::SharedString;
use regex::Regex;
use serde::{Deserialize, Serialize};
use util::paths::CONVERSATIONS_DIR;
use crate::MessageId;
#[derive(Serialize, Deserialize)]
pub struct SavedConversation {
/// The schema version of the conversation.
pub version: String,
/// The title of the conversation, generated by the Assistant.
pub title: String,
pub messages: Vec<SavedChatMessage>,
}
#[derive(Serialize, Deserialize)]
pub enum SavedChatMessage {
User {
id: MessageId,
body: String,
attachments: Vec<SavedUserAttachment>,
},
Assistant {
id: MessageId,
messages: Vec<SavedAssistantMessagePart>,
error: Option<SharedString>,
},
}
#[derive(Serialize, Deserialize)]
pub struct SavedAssistantMessagePart {
pub body: SharedString,
pub tool_calls: Vec<SavedToolFunctionCall>,
}
pub struct SavedConversationMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.\d.\d.\d.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out conversations saved by the old assistant.
if !re.is_match(file_name) {
continue;
}
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}

View File

@@ -0,0 +1,196 @@
use std::sync::Arc;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakView};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use crate::saved_conversation::SavedConversationMetadata;
pub struct SavedConversations {
focus_handle: FocusHandle,
picker: Option<View<Picker<SavedConversationPickerDelegate>>>,
}
impl EventEmitter<DismissEvent> for SavedConversations {}
impl FocusableView for SavedConversations {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
if let Some(picker) = self.picker.as_ref() {
picker.focus_handle(cx)
} else {
self.focus_handle.clone()
}
}
}
impl SavedConversations {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
picker: None,
}
}
pub fn init(
&mut self,
saved_conversations: Vec<SavedConversationMetadata>,
cx: &mut ViewContext<Self>,
) {
let delegate =
SavedConversationPickerDelegate::new(cx.view().downgrade(), saved_conversations);
self.picker = Some(cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)));
}
}
impl Render for SavedConversations {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w_full()
.bg(cx.theme().colors().panel_background)
.children(self.picker.clone())
}
}
pub struct SavedConversationPickerDelegate {
view: WeakView<SavedConversations>,
saved_conversations: Vec<SavedConversationMetadata>,
selected_index: usize,
matches: Vec<StringMatch>,
}
impl SavedConversationPickerDelegate {
pub fn new(
weak_view: WeakView<SavedConversations>,
saved_conversations: Vec<SavedConversationMetadata>,
) -> Self {
let matches = saved_conversations
.iter()
.map(|conversation| StringMatch {
candidate_id: 0,
score: 0.0,
positions: Default::default(),
string: conversation.title.clone(),
})
.collect();
Self {
view: weak_view,
saved_conversations,
selected_index: 0,
matches,
}
}
}
impl PickerDelegate for SavedConversationPickerDelegate {
type ListItem = ui::ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select saved conversation...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let background_executor = cx.background_executor().clone();
let candidates = self
.saved_conversations
.iter()
.enumerate()
.map(|(id, conversation)| {
let text = conversation.title.clone();
StringMatchCandidate {
id,
char_bag: text.as_str().into(),
string: text,
}
})
.collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background_executor,
)
.await
};
this.update(&mut cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(cx);
return;
}
// TODO: Implement selecting a saved conversation.
}
fn dismissed(&mut self, cx: &mut ui::prelude::ViewContext<Picker<Self>>) {
self.view
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let conversation_match = &self.matches[ix];
let _conversation = &self.saved_conversations[conversation_match.candidate_id];
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(HighlightedLabel::new(
conversation_match.string.clone(),
conversation_match.positions.clone(),
)),
)
}
}

View File

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

View File

@@ -0,0 +1,304 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolView};
use editor::{
display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle},
Editor, MultiBuffer,
};
use futures::{channel::mpsc::UnboundedSender, StreamExt as _};
use gpui::{prelude::*, AnyElement, AsyncWindowContext, Model, Task, View, WeakView};
use language::ToPoint;
use project::{search::SearchQuery, Project, ProjectPath};
use schemars::JsonSchema;
use serde::Deserialize;
use std::path::Path;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
pub struct AnnotationTool {
workspace: WeakView<Workspace>,
project: Model<Project>,
}
impl AnnotationTool {
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self { workspace, project }
}
}
#[derive(Default, Debug, Deserialize, JsonSchema, Clone)]
pub struct AnnotationInput {
/// Name for this set of annotations
#[serde(default = "default_title")]
title: String,
/// Excerpts from the file to show to the user.
excerpts: Vec<Excerpt>,
}
fn default_title() -> String {
"Untitled".to_string()
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
struct Excerpt {
/// Path to the file
path: String,
/// A short, distinctive string that appears in the file, used to define a location in the file.
text_passage: String,
/// Text to display above the code excerpt
annotation: String,
}
impl LanguageModelTool for AnnotationTool {
type View = AnnotationResultView;
fn name(&self) -> String {
"annotate_code".to_string()
}
fn description(&self) -> String {
"Dynamically annotate symbols in the current codebase. Opens a buffer in a panel in their editor, to the side of the conversation. The annotations are shown in the editor as a block decoration.".to_string()
}
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|cx| {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
cx.spawn(|view, mut cx| async move {
while let Some(excerpt) = rx.next().await {
AnnotationResultView::add_excerpt(view.clone(), excerpt, &mut cx).await?;
}
anyhow::Ok(())
})
.detach();
AnnotationResultView {
project: self.project.clone(),
workspace: self.workspace.clone(),
tx,
pending_excerpt: None,
added_editor_to_workspace: false,
editor: None,
error: None,
rendered_excerpt_count: 0,
}
})
}
}
pub struct AnnotationResultView {
workspace: WeakView<Workspace>,
project: Model<Project>,
pending_excerpt: Option<Excerpt>,
added_editor_to_workspace: bool,
editor: Option<View<Editor>>,
tx: UnboundedSender<Excerpt>,
error: Option<anyhow::Error>,
rendered_excerpt_count: usize,
}
impl AnnotationResultView {
async fn add_excerpt(
this: WeakView<Self>,
excerpt: Excerpt,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let project = this.update(cx, |this, _cx| this.project.clone())?;
let worktree_id = project.update(cx, |project, cx| {
let worktree = project.worktrees().next()?;
let worktree_id = worktree.read(cx).id();
Some(worktree_id)
})?;
let worktree_id = if let Some(worktree_id) = worktree_id {
worktree_id
} else {
return Err(anyhow::anyhow!("No worktree found"));
};
let buffer_task = project.update(cx, |project, cx| {
project.open_buffer(
ProjectPath {
worktree_id,
path: Path::new(&excerpt.path).into(),
},
cx,
)
})?;
let buffer = match buffer_task.await {
Ok(buffer) => buffer,
Err(error) => {
return this.update(cx, |this, cx| {
this.error = Some(error);
cx.notify();
})
}
};
let snapshot = buffer.update(cx, |buffer, _cx| buffer.snapshot())?;
let query = SearchQuery::text(&excerpt.text_passage, false, false, false, vec![], vec![])?;
let matches = query.search(&snapshot, None).await;
let Some(first_match) = matches.first() else {
log::warn!(
"text {:?} does not appear in '{}'",
excerpt.text_passage,
excerpt.path
);
return Ok(());
};
this.update(cx, |this, cx| {
let mut start = first_match.start.to_point(&snapshot);
start.column = 0;
if let Some(editor) = &this.editor {
editor.update(cx, |editor, cx| {
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.push_excerpts_with_context_lines(
buffer.clone(),
vec![start..start],
5,
cx,
)
});
let annotation = SharedString::from(excerpt.annotation);
editor.insert_blocks(
[BlockProperties {
position: ranges[0].start,
height: annotation.split('\n').count() as u8 + 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| Self::render_note_block(&annotation, cx)),
disposition: BlockDisposition::Above,
}],
None,
cx,
);
});
if !this.added_editor_to_workspace {
this.added_editor_to_workspace = true;
this.workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
})
.log_err();
}
}
})?;
Ok(())
}
fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement {
let anchor_x = cx.anchor_x;
let gutter_width = cx.gutter_dimensions.width;
h_flex()
.w_full()
.py_2()
.border_y_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.justify_center()
.w(gutter_width)
.child(Icon::new(IconName::Ai).color(Color::Hint)),
)
.child(
h_flex()
.w_full()
.ml(anchor_x - gutter_width)
.child(explanation.clone()),
)
.into_any_element()
}
}
impl Render for AnnotationResultView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(error) = &self.error {
ui::Label::new(error.to_string()).into_any_element()
} else {
ui::Label::new(SharedString::from(format!(
"Opened a buffer with {} excerpts",
self.rendered_excerpt_count
)))
.into_any_element()
}
}
}
impl ToolView for AnnotationResultView {
type Input = AnnotationInput;
type SerializedState = Option<String>;
fn generate(&self, _: &mut ProjectContext, _: &mut ViewContext<Self>) -> String {
if let Some(error) = &self.error {
format!("Failed to create buffer: {error:?}")
} else {
format!(
"opened {} excerpts in a buffer",
self.rendered_excerpt_count
)
}
}
fn set_input(&mut self, mut input: Self::Input, cx: &mut ViewContext<Self>) {
let editor = if let Some(editor) = &self.editor {
editor.clone()
} else {
let multibuffer = cx.new_model(|_cx| {
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(String::new())
});
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), cx)
});
self.editor = Some(editor.clone());
editor
};
editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |multibuffer, cx| {
if multibuffer.title(cx) != input.title {
multibuffer.set_title(input.title.clone(), cx);
}
});
self.pending_excerpt = input.excerpts.pop();
for excerpt in input.excerpts.iter().skip(self.rendered_excerpt_count) {
self.tx.unbounded_send(excerpt.clone()).ok();
}
self.rendered_excerpt_count = input.excerpts.len();
});
cx.notify();
}
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
if let Some(excerpt) = self.pending_excerpt.take() {
self.rendered_excerpt_count += 1;
self.tx.unbounded_send(excerpt.clone()).ok();
}
self.tx.close_channel();
Task::ready(Ok(()))
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
self.error.as_ref().map(|error| error.to_string())
}
fn deserialize(
&mut self,
output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
if let Some(error_message) = output {
self.error = Some(anyhow::anyhow!("{}", error_message));
}
Ok(())
}
}

View File

@@ -0,0 +1,145 @@
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolView};
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, Clone, 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 View = CreateBufferView;
fn name(&self) -> String {
"create_file".to_string()
}
fn description(&self) -> String {
"Create a new untitled file in the current codebase. Side effect: opens it in a new pane/tab for the user to edit.".to_string()
}
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {
workspace: self.workspace.clone(),
project: self.project.clone(),
input: None,
error: None,
})
}
}
pub struct CreateBufferView {
workspace: WeakView<Workspace>,
project: Model<Project>,
input: Option<CreateBufferInput>,
error: Option<anyhow::Error>,
}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
ui::Label::new("Opening a buffer")
}
}
impl ToolView for CreateBufferView {
type Input = CreateBufferInput;
type SerializedState = ();
fn generate(&self, _project: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
let Some(input) = self.input.as_ref() else {
return "No input".to_string();
};
match &self.error {
None => format!("Created a new {} buffer", input.language),
Some(err) => format!("Failed to create buffer: {err:?}"),
}
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = Some(input);
cx.notify();
}
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
cx.spawn({
let workspace = self.workspace.clone();
let project = self.project.clone();
let input = self.input.clone();
|_this, mut cx| async move {
let input = input.ok_or_else(|| anyhow!("no input"))?;
let text = input.text.clone();
let language_name = input.language.clone();
let language = cx
.update(|cx| {
project
.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 serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
()
}
fn deserialize(
&mut self,
_output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
Ok(())
}
}

View File

@@ -0,0 +1,428 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ToolView};
use collections::BTreeMap;
use file_icons::FileIcons;
use gpui::{prelude::*, AnyElement, Model, Task};
use project::ProjectPath;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::{Deserialize, Serialize};
use std::{
fmt::Write as _,
ops::Range,
path::{Path, PathBuf},
str::FromStr as _,
sync::Arc,
};
use ui::{prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
const DEFAULT_SEARCH_LIMIT: usize = 20;
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
}
#[derive(Default)]
enum ProjectIndexToolState {
#[default]
CollectingQuery,
Searching,
Error(anyhow::Error),
Finished {
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
index_status: Status,
},
}
pub struct ProjectIndexView {
project_index: Model<ProjectIndex>,
input: CodebaseQuery,
expanded_header: bool,
state: ProjectIndexToolState,
}
#[derive(Default, Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Criteria to include results
includes: Option<SearchFilter>,
/// Criteria to exclude results
excludes: Option<SearchFilter>,
}
#[derive(Deserialize, JsonSchema, Clone, Default)]
pub struct SearchFilter {
/// Filter by file path prefix
prefix_path: Option<String>,
/// Filter by file extension
extension: Option<String>,
// Note: we possibly can't do content filtering very easily given the project context handling
// the final results, so we're leaving out direct string matches for now
}
fn project_starts_with(prefix_path: Option<String>, project_path: ProjectPath) -> bool {
if let Some(path) = &prefix_path {
if let Some(path) = PathBuf::from_str(path).ok() {
return project_path.path.starts_with(path);
}
}
return false;
}
impl SearchFilter {
fn matches(&self, project_path: &ProjectPath) -> bool {
let path_match = project_starts_with(self.prefix_path.clone(), project_path.clone());
path_match
&& (if let Some(extension) = &self.extension {
project_path
.path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == extension)
.unwrap_or(false)
} else {
true
})
}
}
#[derive(Serialize, Deserialize)]
pub struct SerializedState {
index_status: Status,
error_message: Option<String>,
worktrees: BTreeMap<Arc<Path>, WorktreeIndexOutput>,
}
#[derive(Default, Serialize, Deserialize)]
struct WorktreeIndexOutput {
excerpts: BTreeMap<Arc<Path>, Vec<Range<usize>>>,
}
impl ProjectIndexView {
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
self.expanded_header = !self.expanded_header;
cx.notify();
}
fn render_filter_section(
&mut self,
heading: &str,
filter: Option<SearchFilter>,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let filter = match filter {
Some(filter) => filter,
None => return None,
};
// Any of the filter fields can be empty. We'll show nothing if they're all empty.
let path = filter.prefix_path.as_ref().map(|path| {
let icon_path = FileIcons::get_icon(Path::new(path), cx)
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg"));
h_flex()
.gap_1()
.child("Paths: ")
.child(Icon::from_path(icon_path))
.child(ui::Label::new(path.clone()).color(Color::Muted))
});
let extension = filter.extension.as_ref().map(|extension| {
let icon_path = FileIcons::get_icon(Path::new(extension), cx)
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg"));
h_flex()
.gap_1()
.child("Extensions: ")
.child(Icon::from_path(icon_path))
.child(ui::Label::new(extension.clone()).color(Color::Muted))
});
if path.is_none() && extension.is_none() {
return None;
}
Some(
v_flex()
.child(ui::Label::new(heading.to_string()))
.gap_1()
.children(path)
.children(extension)
.into_any_element(),
)
}
}
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let (header_text, content) = match &self.state {
ProjectIndexToolState::Error(error) => {
return format!("failed to search: {error:?}").into_any_element()
}
ProjectIndexToolState::CollectingQuery | ProjectIndexToolState::Searching => {
("Searching...".to_string(), div())
}
ProjectIndexToolState::Finished { excerpts, .. } => {
let file_count = excerpts.len();
if excerpts.is_empty() {
("No results found".to_string(), div())
} else {
let header_text = format!(
"Read {} {}",
file_count,
if file_count == 1 { "file" } else { "files" }
);
let el = v_flex().gap_2().children(excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string()).color(Color::Muted),
)
}));
(header_text, el)
}
}
};
let header = h_flex()
.gap_2()
.child(Icon::new(IconName::File))
.child(header_text);
v_flex()
.gap_3()
.child(
CollapsibleContainer::new("collapsible-container", self.expanded_header)
.start_slot(header)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_header(cx);
}))
.child(
v_flex()
.gap_3()
.p_3()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.children(self.render_filter_section(
"Includes",
self.input.includes.clone(),
cx,
))
.children(self.render_filter_section(
"Excludes",
self.input.excludes.clone(),
cx,
))
.child(content),
),
)
.into_any_element()
}
}
impl ToolView for ProjectIndexView {
type Input = CodebaseQuery;
type SerializedState = SerializedState;
fn generate(
&self,
context: &mut assistant_tooling::ProjectContext,
_: &mut ViewContext<Self>,
) -> String {
match &self.state {
ProjectIndexToolState::CollectingQuery => String::new(),
ProjectIndexToolState::Searching => String::new(),
ProjectIndexToolState::Error(error) => format!("failed to search: {error:?}"),
ProjectIndexToolState::Finished {
excerpts,
index_status,
} => {
let mut body = "found results in the following paths:\n".to_string();
for (project_path, ranges) in excerpts {
context.add_excerpts(project_path.clone(), ranges);
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
}
if *index_status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
body
}
}
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = input;
cx.notify();
}
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.state = ProjectIndexToolState::Searching;
cx.notify();
let project_index = self.project_index.read(cx);
let index_status = project_index.status();
// TODO: wire the filters into the search here instead of processing after.
// Otherwise we'll get zero results sometimes.
let search = project_index.search(self.input.query.clone(), DEFAULT_SEARCH_LIMIT, cx);
let includes = self.input.includes.clone();
let excludes = self.input.excludes.clone();
cx.spawn(|this, mut cx| async move {
let search_result = search.await;
this.update(&mut cx, |this, cx| {
match search_result {
Ok(search_results) => {
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
for search_result in search_results {
let project_path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path,
};
if let Some(includes) = &includes {
if !includes.matches(&project_path) {
continue;
}
} else if let Some(excludes) = &excludes {
if excludes.matches(&project_path) {
continue;
}
}
excerpts
.entry(project_path)
.or_default()
.push(search_result.range);
}
this.state = ProjectIndexToolState::Finished {
excerpts,
index_status,
};
}
Err(error) => {
this.state = ProjectIndexToolState::Error(error);
}
}
cx.notify();
})
})
}
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState {
let mut serialized = SerializedState {
error_message: None,
index_status: Status::Idle,
worktrees: Default::default(),
};
match &self.state {
ProjectIndexToolState::Error(err) => serialized.error_message = Some(err.to_string()),
ProjectIndexToolState::Finished {
excerpts,
index_status,
} => {
serialized.index_status = *index_status;
if let Some(project) = self.project_index.read(cx).project().upgrade() {
let project = project.read(cx);
for (project_path, excerpts) in excerpts {
if let Some(worktree) =
project.worktree_for_id(project_path.worktree_id, cx)
{
let worktree_path = worktree.read(cx).abs_path();
serialized
.worktrees
.entry(worktree_path)
.or_default()
.excerpts
.insert(project_path.path.clone(), excerpts.clone());
}
}
}
}
_ => {}
}
serialized
}
fn deserialize(
&mut self,
serialized: Self::SerializedState,
cx: &mut ViewContext<Self>,
) -> Result<()> {
if !serialized.worktrees.is_empty() {
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
if let Some(project) = self.project_index.read(cx).project().upgrade() {
let project = project.read(cx);
for (worktree_path, worktree_state) in serialized.worktrees {
if let Some(worktree) = project
.worktrees()
.find(|worktree| worktree.read(cx).abs_path() == worktree_path)
{
let worktree_id = worktree.read(cx).id();
for (path, serialized_excerpts) in worktree_state.excerpts {
excerpts.insert(ProjectPath { worktree_id, path }, serialized_excerpts);
}
}
}
}
self.state = ProjectIndexToolState::Finished {
excerpts,
index_status: serialized.index_status,
};
}
cx.notify();
Ok(())
}
}
impl ProjectIndexTool {
pub fn new(project_index: Model<ProjectIndex>) -> Self {
Self { project_index }
}
}
impl LanguageModelTool for ProjectIndexTool {
type View = ProjectIndexView;
fn name(&self) -> String {
"semantic_search_codebase".to_string()
}
fn description(&self) -> String {
unindent::unindent(
r#"This search tool uses a semantic index to perform search queries across your codebase, identifying and returning excerpts of text and code possibly related to the query.
Ideal for:
- Discovering implementations of similar logic within the project
- Finding usage examples of functions, classes/structures, libraries, and other code elements
- Developing understanding of the codebase's architecture and design
Note: The search's effectiveness is directly related to the current state of the codebase and the specificity of your query. It is recommended that you use snippets of code that are similar to the code you wish to find."#,
)
}
fn view(&self, cx: &mut WindowContext) -> gpui::View<Self::View> {
cx.new_view(|_| ProjectIndexView {
state: ProjectIndexToolState::CollectingQuery,
input: Default::default(),
expanded_header: false,
project_index: self.project_index.clone(),
})
}
}

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::*;

View File

@@ -0,0 +1,134 @@
use crate::attachments::ActiveEditorAttachmentTool;
use assistant_tooling::AttachmentRegistry;
use editor::Editor;
use gpui::{prelude::*, Subscription, View};
use std::sync::Arc;
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Tooltip};
use workspace::Workspace;
#[derive(Clone)]
enum Status {
ActiveFile(String),
#[allow(dead_code)]
NoFile,
}
pub struct ActiveFileButton {
attachment_registry: Arc<AttachmentRegistry>,
status: Status,
#[allow(dead_code)]
workspace_subscription: Subscription,
}
impl ActiveFileButton {
pub fn new(
attachment_registry: Arc<AttachmentRegistry>,
workspace: View<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let workspace_subscription = cx.subscribe(&workspace, Self::handle_workspace_event);
cx.defer(move |this, cx| this.update_active_buffer(workspace.clone(), cx));
Self {
attachment_registry,
status: Status::NoFile,
workspace_subscription,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.attachment_registry
.set_attachment_tool_enabled::<ActiveEditorAttachmentTool>(enabled);
}
pub fn update_active_buffer(&mut self, workspace: View<Workspace>, cx: &mut ViewContext<Self>) {
let active_buffer = workspace
.read(cx)
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()));
if let Some(buffer) = active_buffer {
let buffer = buffer.read(cx);
if let Some(singleton) = buffer.as_singleton() {
let singleton = singleton.read(cx);
let filename: String = singleton
.file()
.map(|file| file.path().to_string_lossy())
.unwrap_or("Untitled".into())
.into();
self.status = Status::ActiveFile(filename);
}
}
}
fn handle_workspace_event(
&mut self,
workspace: View<Workspace>,
event: &workspace::Event,
cx: &mut ViewContext<Self>,
) {
if let workspace::Event::ActiveItemChanged = event {
self.update_active_buffer(workspace, cx);
}
}
}
impl Render for ActiveFileButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let is_enabled = self
.attachment_registry
.is_attachment_tool_enabled::<ActiveEditorAttachmentTool>();
let icon = if is_enabled {
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Default)
} else {
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Disabled)
};
let indicator = None;
let status = self.status.clone();
ButtonLike::new("active-file-button")
.child(
ui::IconWithIndicator::new(icon, indicator)
.indicator_border_color(Some(gpui::transparent_black())),
)
.tooltip({
move |cx| {
let status = status.clone();
let (tooltip, meta) = match (is_enabled, status) {
(false, _) => (
"Active file disabled".to_string(),
Some("Click to enable".to_string()),
),
(true, Status::ActiveFile(filename)) => (
format!("Active file {filename} enabled"),
Some("Click to disable".to_string()),
),
(true, Status::NoFile) => {
("No file active for conversation".to_string(), None)
}
};
if let Some(meta) = meta {
Tooltip::with_meta(tooltip, None, meta, cx)
} else {
Tooltip::text(tooltip, cx)
}
}
})
.on_click(cx.listener(move |this, _, cx| {
this.set_enabled(!is_enabled);
cx.notify();
}))
}
}

View File

@@ -0,0 +1,140 @@
use std::sync::Arc;
use client::User;
use gpui::{hsla, AnyElement, ClickEvent};
use ui::{prelude::*, Avatar, Tooltip};
use crate::MessageId;
pub enum UserOrAssistant {
User(Option<Arc<User>>),
Assistant,
}
#[derive(IntoElement)]
pub struct ChatMessage {
id: MessageId,
player: UserOrAssistant,
messages: Vec<AnyElement>,
selected: bool,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
}
impl ChatMessage {
pub fn new(
id: MessageId,
player: UserOrAssistant,
messages: Vec<AnyElement>,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
) -> Self {
Self {
id,
player,
messages,
selected: false,
collapsed,
on_collapse_handle_click,
}
}
}
impl Selectable for ChatMessage {
fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl RenderOnce for ChatMessage {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let message_group = SharedString::from(format!("{}_group", self.id.0));
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
let content_padding = Spacing::Small.rems(cx);
// Clamp the message height to exactly 1.5 lines when collapsed.
let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
let background_color = if let UserOrAssistant::User(_) = &self.player {
Some(cx.theme().colors().surface_background)
} else {
None
};
let (username, avatar_uri) = match self.player {
UserOrAssistant::Assistant => (
"Assistant".into(),
Some("https://zed.dev/assistant_avatar.png".into()),
),
UserOrAssistant::User(Some(user)) => {
(user.github_login.clone(), Some(user.avatar_uri.clone()))
}
UserOrAssistant::User(None) => ("You".into(), None),
};
v_flex()
.group(message_group.clone())
.gap(Spacing::XSmall.rems(cx))
.p(Spacing::XSmall.rems(cx))
.when(self.selected, |element| {
element.bg(hsla(0.6, 0.67, 0.46, 0.12))
})
.rounded_lg()
.child(
h_flex()
.justify_between()
.px(content_padding)
.child(
h_flex()
.gap_2()
.map(|this| {
let avatar_size = rems_from_px(20.);
if let Some(avatar_uri) = avatar_uri {
this.child(Avatar::new(avatar_uri).size(avatar_size))
} else {
this.child(div().size(avatar_size))
}
})
.child(Label::new(username).color(Color::Muted)),
)
.child(
h_flex().visible_on_hover(message_group).child(
// temp icons
IconButton::new(
collapse_handle_id.clone(),
if self.collapsed {
IconName::ArrowUp
} else {
IconName::ArrowDown
},
)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(self.on_collapse_handle_click)
.tooltip(|cx| Tooltip::text("Collapse Message", cx)),
),
),
)
.when(self.messages.len() > 0, |el| {
el.child(
h_flex().w_full().child(
v_flex()
.relative()
.overflow_hidden()
.w_full()
.p(content_padding)
.gap_3()
.text_ui(cx)
.rounded_lg()
.when_some(background_color, |this, background_color| {
this.bg(background_color)
})
.when(self.collapsed, |this| this.h(collapsed_height))
.children(self.messages),
),
)
})
}
}

View File

@@ -0,0 +1,71 @@
use ui::{prelude::*, Avatar, IconButtonShape};
#[derive(IntoElement)]
pub struct ChatNotice {
message: SharedString,
meta: Option<SharedString>,
}
impl ChatNotice {
pub fn new(message: impl Into<SharedString>) -> Self {
Self {
message: message.into(),
meta: None,
}
}
pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
self.meta = Some(meta.into());
self
}
}
impl RenderOnce for ChatNotice {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.w_full()
.items_start()
.mt_4()
.gap_3()
.child(
// TODO: Replace with question mark.
Avatar::new("https://zed.dev/assistant_avatar.png").size(rems_from_px(20.)),
)
.child(
v_flex()
.size_full()
.gap_1()
.pr_4()
.overflow_hidden()
.child(
h_flex()
.justify_between()
.overflow_hidden()
.child(
h_flex()
.flex_none()
.overflow_hidden()
.child(Label::new(self.message)),
)
.child(
h_flex()
.flex_shrink_0()
.gap_1()
.child(Button::new("allow", "Allow"))
.child(
IconButton::new("deny", IconName::Close)
.shape(IconButtonShape::Square)
.icon_color(Color::Muted)
.size(ButtonSize::None)
.icon_size(IconSize::XSmall),
),
),
)
.children(
self.meta.map(|meta| {
Label::new(meta).size(LabelSize::Small).color(Color::Muted)
}),
),
)
}
}

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