Compare commits
3 Commits
path-env
...
workflow-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9404251dd0 | ||
|
|
289ba8fb34 | ||
|
|
ba7e894a6e |
@@ -1,8 +1,8 @@
|
||||
We have two cloudflare workers that let us serve some assets of this repo
|
||||
from Cloudflare.
|
||||
|
||||
- `open-source-website-assets` is used for `install.sh`
|
||||
- `docs-proxy` is used for `https://zed.dev/docs`
|
||||
* `open-source-website-assets` is used for `install.sh`
|
||||
* `docs-proxy` is used for `https://zed.dev/docs`
|
||||
|
||||
On push to `main`, both of these (and the files they depend on) are uploaded to Cloudflare.
|
||||
|
||||
|
||||
@@ -3,15 +3,6 @@ export default {
|
||||
const url = new URL(request.url);
|
||||
url.hostname = "docs-anw.pages.dev";
|
||||
|
||||
// These pages were removed, but may still be served due to Cloudflare's
|
||||
// [asset retention](https://developers.cloudflare.com/pages/configuration/serving-pages/#asset-retention).
|
||||
if (
|
||||
url.pathname === "/docs/assistant/context-servers" ||
|
||||
url.pathname === "/docs/assistant/model-context-protocol"
|
||||
) {
|
||||
return await fetch("https://zed.dev/404");
|
||||
}
|
||||
|
||||
let res = await fetch(url, request);
|
||||
|
||||
if (res.status === 404) {
|
||||
|
||||
21
.github/workflows/ci.yml
vendored
@@ -167,7 +167,6 @@ jobs:
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
@@ -232,20 +231,20 @@ jobs:
|
||||
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- name: Upload app bundle (universal) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
|
||||
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
|
||||
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
|
||||
@@ -277,7 +276,6 @@ jobs:
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
@@ -321,7 +319,7 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
@@ -339,7 +337,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-linux-aarch64: # this runs on ubuntu22.04
|
||||
bundle-linux-aarch64:
|
||||
timeout-minutes: 60
|
||||
name: Create arm64 Linux bundle
|
||||
runs-on:
|
||||
@@ -348,7 +346,6 @@ jobs:
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
@@ -360,10 +357,10 @@ jobs:
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y llvm-15 clang-15 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-15/bin" >> $GITHUB_PATH
|
||||
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@0bf4f07ef9048ec62a45f9dbf2f098afa49695f0 # v1
|
||||
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
@@ -406,7 +403,7 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # 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
|
||||
|
||||
33
.github/workflows/delete_comments.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Delete Mediafire Comments
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
delete_comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for specific strings in comment
|
||||
id: check_comment
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const comment = context.payload.comment.body;
|
||||
const triggerStrings = ['www.mediafire.com'];
|
||||
return triggerStrings.some(triggerString => comment.includes(triggerString));
|
||||
|
||||
- name: Delete comment if it contains any of the specific strings
|
||||
if: steps.check_comment.outputs.result == 'true'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const commentId = context.payload.comment.id;
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: commentId
|
||||
});
|
||||
8
.github/workflows/deploy_cloudflare.yml
vendored
@@ -21,14 +21,6 @@ jobs:
|
||||
with:
|
||||
mdbook-version: "0.4.37"
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/collab-config.toml ./.cargo/config.toml
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libxkbcommon-dev libxkbcommon-x11-dev
|
||||
|
||||
- name: Build book
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
5
.github/workflows/release_nightly.yml
vendored
@@ -67,7 +67,6 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
@@ -107,7 +106,6 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
@@ -141,7 +139,6 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
@@ -157,7 +154,7 @@ jobs:
|
||||
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
|
||||
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: rui314/setup-mold@0bf4f07ef9048ec62a45f9dbf2f098afa49695f0 # v1
|
||||
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5
|
||||
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: "x64"
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5
|
||||
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: "x64"
|
||||
|
||||
20
.mailmap
@@ -11,12 +11,8 @@
|
||||
|
||||
Alex Viscreanu <alexviscreanu@gmail.com>
|
||||
Alex Viscreanu <alexviscreanu@gmail.com> <alexandru.viscreanu@kiwi.com>
|
||||
Alexander Mankuta <alex@pointless.one>
|
||||
Alexander Mankuta <alex@pointless.one> <alex+github@pointless.one>
|
||||
amtoaer <amtoaer@gmail.com>
|
||||
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
|
||||
Andrei Zvonimir Crnković <andrei@0x7f.dev>
|
||||
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
|
||||
Antonio Scandurra <me@as-cii.com>
|
||||
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
|
||||
Bennet Bo Fenner <bennet@zed.dev>
|
||||
@@ -28,9 +24,7 @@ Conrad Irwin <conrad@zed.dev>
|
||||
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
|
||||
Danilo Leal <danilo@zed.dev>
|
||||
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
|
||||
Evren Sen <nervenes@icloud.com>
|
||||
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
|
||||
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.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,12 +48,8 @@ LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
|
||||
Marshall Bowers <elliott.codes@gmail.com>
|
||||
Marshall Bowers <elliott.codes@gmail.com> <marshall@zed.dev>
|
||||
Matt Fellenz <matt@felle.nz>
|
||||
Matt Fellenz <matt@felle.nz> <matt+github@felle.nz>
|
||||
Max Brunsfeld <maxbrunsfeld@gmail.com>
|
||||
Max Brunsfeld <maxbrunsfeld@gmail.com> <max@zed.dev>
|
||||
Max Linke <maxlinke88@gmail.com>
|
||||
Max Linke <maxlinke88@gmail.com> <kain88-de@users.noreply.github.com>
|
||||
Mikayla Maki <mikayla@zed.dev>
|
||||
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
|
||||
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
|
||||
@@ -84,18 +74,10 @@ 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>
|
||||
Roy Williams <roy.williams.iii@gmail.com>
|
||||
Roy Williams <roy.williams.iii@gmail.com> <roy@anthropic.com>
|
||||
Sergey Onufrienko <sergey@onufrienko.com>
|
||||
Thorben Kröger <dev@thorben.net>
|
||||
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
|
||||
Thorsten Ball <thorsten@zed.dev>
|
||||
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
|
||||
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
|
||||
Tristan Hume <tris.hume@gmail.com>
|
||||
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
|
||||
Uladzislau Kaminski <i@uladkaminski.com>
|
||||
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
|
||||
WindSoilder <WindSoilder@outlook.com>
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"hard_tabs": false,
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
{
|
||||
"label": "clippy",
|
||||
"command": "./script/clippy",
|
||||
"args": [],
|
||||
"allow_concurrent_runs": true,
|
||||
"use_new_terminal": false
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"label": "cargo run --profile release-fast",
|
||||
"command": "cargo",
|
||||
"args": ["run", "--profile", "release-fast"],
|
||||
"allow_concurrent_runs": true,
|
||||
"use_new_terminal": false
|
||||
"args": ["run", "--profile", "release-fast"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@ If you're looking for ideas about what to work on, check out:
|
||||
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
|
||||
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
|
||||
|
||||
For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
|
||||
For adding themes or support for a new language to Zed, check out our [extension docs](https://github.com/zed-industries/extensions/blob/main/AUTHORING_EXTENSIONS.md).
|
||||
|
||||
## Proposing changes
|
||||
|
||||
|
||||
1478
Cargo.lock
generated
42
Cargo.toml
@@ -6,7 +6,6 @@ members = [
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_tool",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -25,7 +24,6 @@ members = [
|
||||
"crates/db",
|
||||
"crates/dev_server_projects",
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/editor",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
@@ -147,7 +145,6 @@ members = [
|
||||
"extensions/lua",
|
||||
"extensions/ocaml",
|
||||
"extensions/php",
|
||||
"extensions/perplexity",
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
"extensions/ruff",
|
||||
@@ -181,7 +178,6 @@ anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tool = { path = "crates/assistant_tool" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
@@ -306,7 +302,7 @@ zed_actions = { path = "crates/zed_actions" }
|
||||
#
|
||||
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91d034ff8b53867143c005acfaa14609147c9a2c" }
|
||||
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "cacdb5bb3b72bad2c729227537979d95af75978f" }
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
ashpd = "0.9.1"
|
||||
@@ -315,22 +311,22 @@ async-dispatcher = "0.1"
|
||||
async-fs = "1.6"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.5.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.23"
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
|
||||
cargo_metadata = "0.18"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = "0.11.6"
|
||||
cocoa = "0.26"
|
||||
cocoa = "0.25"
|
||||
core-foundation = "0.9.3"
|
||||
core-foundation-sys = "0.8.6"
|
||||
ctor = "0.2.6"
|
||||
@@ -340,7 +336,7 @@ dirs = "4.0"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
exec = "0.3.1"
|
||||
fork = "0.2.0"
|
||||
fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
@@ -358,14 +354,15 @@ indoc = "2"
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
] }
|
||||
itertools = "0.13.0"
|
||||
itertools = "0.11.0"
|
||||
jsonwebtoken = "9.3"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nix = "0.29"
|
||||
nix = "0.28"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
ordered-float = "2.1.1"
|
||||
@@ -378,17 +375,18 @@ pretty_assertions = "1.3.0"
|
||||
prost = "0.9"
|
||||
prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
pulldown-cmark = { version = "0.10.0", default-features = false }
|
||||
rand = "0.8.5"
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.15", default-features = false, features = [
|
||||
runtimelib = { version = "0.14", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
schemars = { version = "0.8", features = ["impl_json_schema"] }
|
||||
schemars = {version = "0.8", features = ["impl_json_schema"]}
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
@@ -466,7 +464,7 @@ which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
version = "0.39"
|
||||
version = "0.38"
|
||||
default-features = false
|
||||
features = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
@@ -545,7 +543,6 @@ zed = { codegen-units = 16 }
|
||||
|
||||
[profile.release-fast]
|
||||
inherits = "release"
|
||||
debug = "full"
|
||||
lto = false
|
||||
codegen-units = 16
|
||||
|
||||
@@ -572,17 +569,8 @@ single_range_in_vec_init = "allow"
|
||||
# allow all of those, for now.
|
||||
style = { level = "allow", priority = -1 }
|
||||
|
||||
# Temporary list of style lints that we've fixed so far.
|
||||
module_inception = { level = "deny" }
|
||||
question_mark = { level = "deny" }
|
||||
redundant_closure = { level = "deny" }
|
||||
# Individual rules that have violations in the codebase:
|
||||
type_complexity = "allow"
|
||||
# We often return trait objects from `new` functions.
|
||||
new_ret_no_self = { level = "allow" }
|
||||
# We have a few `next` functions that differ in lifetimes
|
||||
# compared to Iterator::next. Yet, clippy complains about those.
|
||||
should_implement_trait = { level = "allow" }
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.81-bookworm as builder
|
||||
FROM rust:1.80-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
---
|
||||
--------
|
||||
|
||||
### Installation
|
||||
|
||||
|
||||
<a href="https://repology.org/project/zed-editor/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/zed-editor.svg?minversion=0.143.5" alt="Packaging status" align="right">
|
||||
</a>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="2" fill="black" fill-opacity="0.2"/>
|
||||
<g clip-path="url(#clip0_1916_18)">
|
||||
<path d="M10.652 3.79999H8.816L12.164 12.2H14L10.652 3.79999Z" fill="#1F1F1E"/>
|
||||
<path d="M5.348 3.79999L2 12.2H3.872L4.55672 10.436H8.05927L8.744 12.2H10.616L7.268 3.79999H5.348ZM5.16224 8.87599L6.308 5.92399L7.45374 8.87599H5.16224Z" fill="#1F1F1E"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1916_18">
|
||||
<rect width="12" height="8.4" fill="white" transform="translate(2 3.79999)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 601 B |
@@ -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-database-zap"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 15 21.84"/><path d="M21 5V8"/><path d="M21 12L18 17H22L19 22"/><path d="M3 12A9 3 0 0 0 14.59 14.87"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 10V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.71429 7.088C6.7142 7.26164 6.66963 7.43181 6.58559 7.57938C6.50156 7.72695 6.38138 7.84606 6.23857 7.92333L5.47571 8.34333C5.33291 8.4206 5.21273 8.53972 5.12869 8.68729C5.04465 8.83486 5.00008 9.00503 5 9.17867V9.53333C5 9.6571 5.04515 9.7758 5.12553 9.86332C5.2059 9.95083 5.31491 10 5.42857 10H10.5714C10.6851 10 10.7941 9.95083 10.8745 9.86332C10.9548 9.7758 11 9.6571 11 9.53333V9.17867C10.9999 9.00503 10.9553 8.83486 10.8713 8.68729C10.7873 8.53972 10.6671 8.4206 10.5243 8.34333L9.76143 7.92333C9.61862 7.84606 9.49844 7.72695 9.41441 7.57938C9.33037 7.43181 9.2858 7.26164 9.28571 7.088V5.33333C9.28571 5.20957 9.33087 5.09087 9.41124 5.00335C9.49161 4.91583 9.60062 4.86667 9.71429 4.86667C9.94161 4.86667 10.1596 4.76833 10.3204 4.5933C10.4811 4.41827 10.5714 4.18087 10.5714 3.93333C10.5714 3.6858 10.4811 3.4484 10.3204 3.27337C10.1596 3.09833 9.94161 3 9.71429 3H6.28571C6.05839 3 5.84037 3.09833 5.67962 3.27337C5.51888 3.4484 5.42857 3.6858 5.42857 3.93333C5.42857 4.18087 5.51888 4.41827 5.67962 4.5933C5.84037 4.76833 6.05839 4.86667 6.28571 4.86667C6.39938 4.86667 6.50839 4.91583 6.58876 5.00335C6.66913 5.09087 6.71429 5.20957 6.71429 5.33333V7.088Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 4L12 8L5 12V4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.24182 2.32181C3.3919 2.23132 3.5784 2.22601 3.73338 2.30781L12.7334 7.05781C12.8974 7.14436 13 7.31457 13 7.5C13 7.68543 12.8974 7.85564 12.7334 7.94219L3.73338 12.6922C3.5784 12.774 3.3919 12.7687 3.24182 12.6782C3.09175 12.5877 3 12.4252 3 12.25V2.75C3 2.57476 3.09175 2.4123 3.24182 2.32181ZM4 3.57925V11.4207L11.4288 7.5L4 3.57925Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 518 B |
@@ -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-pocket-knife"><path d="M3 2v1c0 1 2 1 2 2S3 6 3 7s2 1 2 2-2 1-2 2 2 1 2 2"/><path d="M18 6h.01"/><path d="M6 18h.01"/><path d="M20.83 8.83a4 4 0 0 0-5.66-5.66l-12 12a4 4 0 1 0 5.66 5.66Z"/><path d="M18 11.66V22a4 4 0 0 0 4-4V6"/></svg>
|
||||
|
Before Width: | Height: | Size: 438 B |
@@ -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-search-code"><path d="m13 13.5 2-2.5-2-2.5"/><path d="m21 21-4.3-4.3"/><path d="M9 8.5 7 11l2 2.5"/><circle cx="11" cy="11" r="8"/></svg>
|
||||
|
Before Width: | Height: | Size: 340 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.28561 2.25C5.83777 2.25 5.42262 2.44429 5.12712 2.76607C4.8336 3.08568 4.67847 3.50652 4.67847 3.93333C4.67847 4.00474 4.68281 4.07598 4.69142 4.14661L10.0356 6.63864V5.58279C10.3579 5.51401 10.6495 5.3435 10.8728 5.10053C11.1662 4.78092 11.3213 4.36012 11.3213 3.93333C11.3213 3.50655 11.1661 3.08559 10.8726 2.76598C10.5771 2.44432 10.1621 2.25 9.71419 2.25H6.28561Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.70131 4.68303C1.87636 4.30763 2.3226 4.14521 2.698 4.32027L13.9361 9.5607C14.3115 9.73575 14.474 10.182 14.2989 10.5574C14.1239 10.9328 13.6776 11.0952 13.3022 10.9202L11.6006 10.1267C11.5536 10.2137 11.4958 10.2957 11.4272 10.3704C11.2121 10.6045 10.9057 10.75 10.5715 10.75H8.75007V13C8.75007 13.4142 8.41428 13.75 8.00007 13.75C7.58585 13.75 7.25007 13.4142 7.25007 13V10.75H5.42864C5.09445 10.75 4.78832 10.6049 4.57321 10.3706C4.36002 10.1385 4.25007 9.83637 4.25007 9.53334V9.17868C4.25021 8.87805 4.32715 8.57932 4.47703 8.31614C4.6266 8.05351 4.84546 7.83234 5.11609 7.68521L5.68873 7.36993L2.06407 5.67973C1.68867 5.50467 1.52625 5.05844 1.70131 4.68303Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -59,8 +59,11 @@
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-insert": "editor::Copy",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"shift-insert": "editor::Paste",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
@@ -109,15 +112,6 @@
|
||||
"alt-g b": "editor::ToggleGitBlame"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Separate block with same context so these display in context menus
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"ctrl-v": "editor::Paste"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
@@ -169,8 +163,7 @@
|
||||
"ctrl-shift-g": "search::SelectPrevMatch",
|
||||
"alt-m": "assistant::ToggleModelSelector",
|
||||
"ctrl-k h": "assistant::DeployHistory",
|
||||
"ctrl-k l": "assistant::DeployPromptLibrary",
|
||||
"ctrl-n": "assistant::NewContext"
|
||||
"ctrl-k l": "assistant::DeployPromptLibrary"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -262,8 +255,7 @@
|
||||
"alt-r": "search::ToggleRegex",
|
||||
"alt-ctrl-f": "project_search::ToggleFilters",
|
||||
"ctrl-alt-shift-r": "search::ToggleRegex",
|
||||
"ctrl-alt-shift-x": "search::ToggleRegex",
|
||||
"ctrl-k shift-enter": "pane::TogglePinTab"
|
||||
"ctrl-alt-shift-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
@@ -523,7 +515,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel && not_editing",
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
@@ -531,7 +523,7 @@
|
||||
"ctrl-alt-c": "outline_panel::CopyPath",
|
||||
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
|
||||
"alt-ctrl-r": "outline_panel::RevealInFileManager",
|
||||
"space": ["outline_panel::Open", { "change_selection": false }],
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev"
|
||||
}
|
||||
@@ -543,34 +535,27 @@
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-n": "project_panel::NewFile",
|
||||
"alt-ctrl-n": "project_panel::NewDirectory",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"ctrl-insert": "project_panel::Copy",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"shift-insert": "project_panel::Paste",
|
||||
"ctrl-alt-c": "project_panel::CopyPath",
|
||||
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Separate block with same context so these display in context menus
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"f2": "project_panel::Rename",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
@@ -626,15 +611,13 @@
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"ctrl-insert": "terminal::Copy",
|
||||
// "ctrl-a": "editor::SelectAll", // conflicts with readline
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-shift-a": "editor::SelectAll",
|
||||
"ctrl-shift-f": "buffer_search::Deploy",
|
||||
"ctrl-shift-l": "terminal::Clear",
|
||||
"ctrl-shift-w": "pane::CloseActiveItem",
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||
@@ -650,13 +633,5 @@
|
||||
"shift-home": "terminal::ScrollToTop",
|
||||
"shift-end": "terminal::ScrollToBottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Separate block with same context so these display in context menus
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"ctrl-shift-v": "terminal::Paste"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -191,8 +191,7 @@
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"alt-m": "assistant::ToggleModelSelector",
|
||||
"cmd-k h": "assistant::DeployHistory",
|
||||
"cmd-k l": "assistant::DeployPromptLibrary",
|
||||
"cmd-n": "assistant::NewContext"
|
||||
"cmd-k l": "assistant::DeployPromptLibrary"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -299,8 +298,7 @@
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
"alt-cmd-f": "project_search::ToggleFilters",
|
||||
"alt-cmd-x": "search::ToggleRegex",
|
||||
"cmd-k shift-enter": "pane::TogglePinTab"
|
||||
"alt-cmd-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
@@ -530,7 +528,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel && not_editing",
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
@@ -538,7 +536,7 @@
|
||||
"cmd-alt-c": "outline_panel::CopyPath",
|
||||
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
|
||||
"alt-cmd-r": "outline_panel::RevealInFileManager",
|
||||
"space": ["outline_panel::Open", { "change_selection": false }],
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev"
|
||||
}
|
||||
@@ -563,8 +561,8 @@
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
|
||||
@@ -41,16 +41,7 @@
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"f4": "search::SelectNextMatch",
|
||||
"shift-f4": "search::SelectPrevMatch",
|
||||
"alt-1": ["pane::ActivateItem", 0],
|
||||
"alt-2": ["pane::ActivateItem", 1],
|
||||
"alt-3": ["pane::ActivateItem", 2],
|
||||
"alt-4": ["pane::ActivateItem", 3],
|
||||
"alt-5": ["pane::ActivateItem", 4],
|
||||
"alt-6": ["pane::ActivateItem", 5],
|
||||
"alt-7": ["pane::ActivateItem", 6],
|
||||
"alt-8": ["pane::ActivateItem", 7],
|
||||
"alt-9": "pane::ActivateLastItem"
|
||||
"shift-f4": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Default Keymap (Atom) for Zed on macOS
|
||||
// Default Keymap (Atom) for Zed on MacOS
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
|
||||
@@ -45,16 +45,7 @@
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"f4": "search::SelectNextMatch",
|
||||
"shift-f4": "search::SelectPrevMatch",
|
||||
"cmd-1": ["pane::ActivateItem", 0],
|
||||
"cmd-2": ["pane::ActivateItem", 1],
|
||||
"cmd-3": ["pane::ActivateItem", 2],
|
||||
"cmd-4": ["pane::ActivateItem", 3],
|
||||
"cmd-5": ["pane::ActivateItem", 4],
|
||||
"cmd-6": ["pane::ActivateItem", 5],
|
||||
"cmd-7": ["pane::ActivateItem", 6],
|
||||
"cmd-8": ["pane::ActivateItem", 7],
|
||||
"cmd-9": "pane::ActivateLastItem"
|
||||
"shift-f4": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
"shift-g": "vim::EndOfDocument",
|
||||
"{": "vim::StartOfParagraph",
|
||||
"}": "vim::EndOfParagraph",
|
||||
"(": "vim::SentenceBackward",
|
||||
")": "vim::SentenceForward",
|
||||
"|": "vim::GoToColumn",
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
@@ -94,7 +92,6 @@
|
||||
"g y": "editor::GoToTypeDefinition",
|
||||
"g shift-i": "editor::GoToImplementation",
|
||||
"g x": "editor::OpenUrl",
|
||||
"g f": "editor::OpenFile",
|
||||
"g n": "vim::SelectNextMatch",
|
||||
"g shift-n": "vim::SelectPreviousMatch",
|
||||
"g l": "vim::SelectNext",
|
||||
@@ -179,19 +176,19 @@
|
||||
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w v": "pane::SplitVertical",
|
||||
"ctrl-w ctrl-v": "pane::SplitVertical",
|
||||
"ctrl-w s": "pane::SplitHorizontal",
|
||||
"ctrl-w shift-s": "pane::SplitHorizontal",
|
||||
"ctrl-w ctrl-s": "pane::SplitHorizontal",
|
||||
"ctrl-w v": "pane::SplitLeft",
|
||||
"ctrl-w ctrl-v": "pane::SplitLeft",
|
||||
"ctrl-w s": "pane::SplitUp",
|
||||
"ctrl-w shift-s": "pane::SplitUp",
|
||||
"ctrl-w ctrl-s": "pane::SplitUp",
|
||||
"ctrl-w c": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-c": "pane::CloseAllItems",
|
||||
"ctrl-w q": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
||||
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
|
||||
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
|
||||
"ctrl-w d": "editor::GoToDefinitionSplit",
|
||||
"ctrl-w g d": "editor::GoToDefinitionSplit",
|
||||
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
|
||||
@@ -214,7 +211,7 @@
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"y": ["vim::PushOperator", "Yank"],
|
||||
"shift-y": "vim::YankLine",
|
||||
"shift-y": "vim::YankToEndOfLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
@@ -326,13 +323,6 @@
|
||||
"ctrl-r": ["vim::PushOperator", "Register"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::ShowCompletions",
|
||||
"ctrl-n": "editor::ShowCompletions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == replace",
|
||||
"bindings": {
|
||||
@@ -401,9 +391,7 @@
|
||||
"context": "vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"],
|
||||
"o": "editor::ToggleHunkDiff", // "d o"
|
||||
"p": "editor::RevertSelectedHunks" // "d p"
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -493,7 +481,6 @@
|
||||
"v": "project_panel::OpenPermanent",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"s": "project_panel::OpenWithSystem",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent",
|
||||
@@ -501,7 +488,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel && not_editing",
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrev",
|
||||
|
||||
@@ -1,61 +1,426 @@
|
||||
{{#if language_name}}
|
||||
Here's a file of {{language_name}} that I'm going to ask you to make an edit to.
|
||||
{{else}}
|
||||
Here's a file of text that I'm going to ask you to make an edit to.
|
||||
{{/if}}
|
||||
You are an expert developer assistant working in an AI-enabled text editor.
|
||||
Your task is to rewrite a specific section of the provided document based on a user-provided prompt.
|
||||
|
||||
{{#if is_insert}}
|
||||
The point you'll need to insert at is marked with <insert_here></insert_here>.
|
||||
{{else}}
|
||||
The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.
|
||||
{{/if}}
|
||||
<guidelines>
|
||||
1. Scope: Modify only content within <rewrite_this> tags. Do not alter anything outside these boundaries.
|
||||
2. Precision: Make changes strictly necessary to fulfill the given prompt. Preserve all other content as-is.
|
||||
3. Seamless integration: Ensure rewritten sections flow naturally with surrounding text and maintain document structure.
|
||||
4. Tag exclusion: Never include <rewrite_this>, </rewrite_this>, <edit_here>, or <insert_here> tags in the output.
|
||||
5. Indentation: Maintain the original indentation level of the file in rewritten sections.
|
||||
6. Completeness: Rewrite the entire tagged section, even if only partial changes are needed. Avoid omissions or elisions.
|
||||
7. Insertions: Replace <insert_here></insert_here> tags with appropriate content as specified by the prompt.
|
||||
8. Code integrity: Respect existing code structure and functionality when making changes.
|
||||
9. Consistency: Maintain a uniform style and tone throughout the rewritten text.
|
||||
</guidelines>
|
||||
|
||||
<examples>
|
||||
<example>
|
||||
<input>
|
||||
<document>
|
||||
{{{document_content}}}
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::cmp;
|
||||
|
||||
<rewrite_this>
|
||||
<insert_here></insert_here>
|
||||
</rewrite_this>
|
||||
pub struct LruCache<K, V> {
|
||||
/// The maximum number of items the cache can hold.
|
||||
capacity: usize,
|
||||
/// The map storing the cached items.
|
||||
items: HashMap<K, V>,
|
||||
}
|
||||
|
||||
// The rest of the implementation...
|
||||
</document>
|
||||
<prompt>
|
||||
doc this
|
||||
</prompt>
|
||||
</input>
|
||||
|
||||
<incorrect_output failure="Over-generation. The text starting with `pub struct AabbTree<T> {` is *after* the rewrite_this tag">
|
||||
/// Represents an Axis-Aligned Bounding Box (AABB) tree data structure.
|
||||
///
|
||||
/// This structure is used for efficient spatial queries and collision detection.
|
||||
/// It organizes objects in a hierarchical tree structure based on their bounding boxes.
|
||||
///
|
||||
/// # Type Parameters
|
||||
///
|
||||
/// * `T`: The type of data associated with each node in the tree.
|
||||
pub struct AabbTree<T> {
|
||||
root: Option<usize>,
|
||||
</incorrect_output>
|
||||
<corrected_output improvement="Generation stops before repeating content after the rewrite_this section">
|
||||
/// Represents an Axis-Aligned Bounding Box (AABB) tree data structure.
|
||||
///
|
||||
/// This structure is used for efficient spatial queries and collision detection.
|
||||
/// It organizes objects in a hierarchical tree structure based on their bounding boxes.
|
||||
///
|
||||
/// # Type Parameters
|
||||
///
|
||||
/// * `T`: The type of data associated with each node in the tree.
|
||||
</corrected_output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<input>
|
||||
<document>
|
||||
import math
|
||||
|
||||
def calculate_circle_area(radius):
|
||||
"""Calculate the area of a circle given its radius."""
|
||||
return math.pi * radius ** 2
|
||||
|
||||
<rewrite_this>
|
||||
<insert_here></insert_here>
|
||||
</rewrite_this>
|
||||
class Circle:
|
||||
def __init__(self, radius):
|
||||
self.radius = radius
|
||||
|
||||
def area(self):
|
||||
return math.pi * self.radius ** 2
|
||||
|
||||
def circumference(self):
|
||||
return 2 * math.pi * self.radius
|
||||
|
||||
# Usage example
|
||||
circle = Circle(5)
|
||||
print(f"Area: {circle.area():.2f}")
|
||||
print(f"Circumference: {circle.circumference():.2f}")
|
||||
</document>
|
||||
<prompt>
|
||||
write docs
|
||||
</prompt>
|
||||
</input>
|
||||
|
||||
<incorrect_output failure="Over-generation. The text starting with `class Circle:` is *after* the rewrite_this tag">
|
||||
"""
|
||||
Represents a circle with methods to calculate its area and circumference.
|
||||
|
||||
This class provides a simple way to work with circles in a geometric context.
|
||||
It allows for the creation of Circle objects with a specified radius and
|
||||
offers methods to compute the circle's area and circumference.
|
||||
|
||||
Attributes:
|
||||
radius (float): The radius of the circle.
|
||||
|
||||
Methods:
|
||||
area(): Calculates and returns the area of the circle.
|
||||
circumference(): Calculates and returns the circumference of the circle.
|
||||
"""
|
||||
class Circle:
|
||||
</incorrect_output>
|
||||
<corrected_output improvement="Generation stops before repeating content after the rewrite_this section">
|
||||
"""
|
||||
Represents a circle with methods to calculate its area and circumference.
|
||||
|
||||
This class provides a simple way to work with circles in a geometric context.
|
||||
It allows for the creation of Circle objects with a specified radius and
|
||||
offers methods to compute the circle's area and circumference.
|
||||
|
||||
Attributes:
|
||||
radius (float): The radius of the circle.
|
||||
|
||||
Methods:
|
||||
area(): Calculates and returns the area of the circle.
|
||||
circumference(): Calculates and returns the circumference of the circle.
|
||||
"""
|
||||
</corrected_output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<input>
|
||||
<document>
|
||||
class BankAccount {
|
||||
private balance: number;
|
||||
|
||||
constructor(initialBalance: number) {
|
||||
this.balance = initialBalance;
|
||||
}
|
||||
|
||||
<rewrite_this>
|
||||
<insert_here></insert_here>
|
||||
</rewrite_this>
|
||||
deposit(amount: number): void {
|
||||
if (amount > 0) {
|
||||
this.balance += amount;
|
||||
}
|
||||
}
|
||||
|
||||
withdraw(amount: number): boolean {
|
||||
if (amount > 0 && this.balance >= amount) {
|
||||
this.balance -= amount;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getBalance(): number {
|
||||
return this.balance;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const account = new BankAccount(1000);
|
||||
account.deposit(500);
|
||||
console.log(account.getBalance()); // 1500
|
||||
account.withdraw(200);
|
||||
console.log(account.getBalance()); // 1300
|
||||
</document>
|
||||
<prompt>
|
||||
//
|
||||
</prompt>
|
||||
</input>
|
||||
|
||||
<incorrect_output failure="Over-generation. The text starting with `deposit(amount: number): void {` is *after* the rewrite_this tag">
|
||||
/**
|
||||
* Deposits the specified amount into the bank account.
|
||||
*
|
||||
* @param amount The amount to deposit. Must be a positive number.
|
||||
* @throws Error if the amount is not positive.
|
||||
*/
|
||||
deposit(amount: number): void {
|
||||
if (amount > 0) {
|
||||
this.balance += amount;
|
||||
} else {
|
||||
throw new Error("Deposit amount must be positive");
|
||||
}
|
||||
}
|
||||
</incorrect_output>
|
||||
<corrected_output improvement="Generation stops before repeating content after the rewrite_this section">
|
||||
/**
|
||||
* Deposits the specified amount into the bank account.
|
||||
*
|
||||
* @param amount The amount to deposit. Must be a positive number.
|
||||
* @throws Error if the amount is not positive.
|
||||
*/
|
||||
</corrected_output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<input>
|
||||
<document>
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct BinaryTree<T> {
|
||||
root: Option<Node<T>>,
|
||||
}
|
||||
|
||||
<rewrite_this>
|
||||
<insert_here></insert_here>
|
||||
</rewrite_this>
|
||||
struct Node<T> {
|
||||
value: T,
|
||||
left: Option<Box<Node<T>>>,
|
||||
right: Option<Box<Node<T>>>,
|
||||
}
|
||||
</document>
|
||||
<prompt>
|
||||
derive clone
|
||||
</prompt>
|
||||
</input>
|
||||
|
||||
<incorrect_output failure="Over-generation below the rewrite_this tags. Extra space between derive annotation and struct definition.">
|
||||
#[derive(Clone)]
|
||||
|
||||
struct Node<T> {
|
||||
value: T,
|
||||
left: Option<Box<Node<T>>>,
|
||||
right: Option<Box<Node<T>>>,
|
||||
}
|
||||
</incorrect_output>
|
||||
|
||||
<incorrect_output failure="Over-generation above the rewrite_this tags">
|
||||
pub struct BinaryTree<T> {
|
||||
root: Option<Node<T>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
</incorrect_output>
|
||||
|
||||
<incorrect_output failure="Over-generation below the rewrite_this tags">
|
||||
#[derive(Clone)]
|
||||
struct Node<T> {
|
||||
value: T,
|
||||
left: Option<Box<Node<T>>>,
|
||||
right: Option<Box<Node<T>>>,
|
||||
}
|
||||
|
||||
impl<T> Node<T> {
|
||||
fn new(value: T) -> Self {
|
||||
Node {
|
||||
value,
|
||||
left: None,
|
||||
right: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
</incorrect_output>
|
||||
<corrected_output improvement="Only includes the new content within the rewrite_this tags">
|
||||
#[derive(Clone)]
|
||||
</corrected_output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<input>
|
||||
<document>
|
||||
import math
|
||||
|
||||
def calculate_circle_area(radius):
|
||||
"""Calculate the area of a circle given its radius."""
|
||||
return math.pi * radius ** 2
|
||||
|
||||
<rewrite_this>
|
||||
<insert_here></insert_here>
|
||||
</rewrite_this>
|
||||
class Circle:
|
||||
def __init__(self, radius):
|
||||
self.radius = radius
|
||||
|
||||
def area(self):
|
||||
return math.pi * self.radius ** 2
|
||||
|
||||
def circumference(self):
|
||||
return 2 * math.pi * self.radius
|
||||
|
||||
# Usage example
|
||||
circle = Circle(5)
|
||||
print(f"Area: {circle.area():.2f}")
|
||||
print(f"Circumference: {circle.circumference():.2f}")
|
||||
</document>
|
||||
<prompt>
|
||||
add dataclass decorator
|
||||
</prompt>
|
||||
</input>
|
||||
|
||||
<incorrect_output failure="Over-generation. The text starting with `class Circle:` is *after* the rewrite_this tag">
|
||||
@dataclass
|
||||
class Circle:
|
||||
radius: float
|
||||
|
||||
def __init__(self, radius):
|
||||
self.radius = radius
|
||||
|
||||
def area(self):
|
||||
return math.pi * self.radius ** 2
|
||||
</incorrect_output>
|
||||
<corrected_output improvement="Generation stops before repeating content after the rewrite_this section">
|
||||
@dataclass
|
||||
</corrected_output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<input>
|
||||
<document>
|
||||
interface ShoppingCart {
|
||||
items: string[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
<rewrite_this>
|
||||
<insert_here></insert_here>class ShoppingCartManager {
|
||||
</rewrite_this>
|
||||
private cart: ShoppingCart;
|
||||
|
||||
constructor() {
|
||||
this.cart = { items: [], total: 0 };
|
||||
}
|
||||
|
||||
addItem(item: string, price: number): void {
|
||||
this.cart.items.push(item);
|
||||
this.cart.total += price;
|
||||
}
|
||||
|
||||
getTotal(): number {
|
||||
return this.cart.total;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const manager = new ShoppingCartManager();
|
||||
manager.addItem("Book", 15.99);
|
||||
console.log(manager.getTotal()); // 15.99
|
||||
</document>
|
||||
<prompt>
|
||||
add readonly modifier
|
||||
</prompt>
|
||||
</input>
|
||||
|
||||
<incorrect_output failure="Over-generation. The line starting with ` items: string[];` is *after* the rewrite_this tag">
|
||||
readonly interface ShoppingCart {
|
||||
items: string[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
class ShoppingCartManager {
|
||||
private readonly cart: ShoppingCart;
|
||||
|
||||
constructor() {
|
||||
this.cart = { items: [], total: 0 };
|
||||
}
|
||||
</incorrect_output>
|
||||
<corrected_output improvement="Only includes the new content within the rewrite_this tags and integrates cleanly into surrounding code">
|
||||
readonly interface ShoppingCart {
|
||||
</corrected_output>
|
||||
</example>
|
||||
|
||||
</examples>
|
||||
|
||||
With these examples in mind, edit the following file:
|
||||
|
||||
<document language="{{ language_name }}">
|
||||
{{{ document_content }}}
|
||||
</document>
|
||||
|
||||
{{#if is_truncated}}
|
||||
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
|
||||
The provided document has been truncated (potentially mid-line) for brevity.
|
||||
{{/if}}
|
||||
|
||||
{{#if is_insert}}
|
||||
You can't replace {{content_type}}, your answer will be inserted in place of the `<insert_here></insert_here>` tags. Don't include the insert_here tags in your output.
|
||||
|
||||
Generate {{content_type}} based on the following prompt:
|
||||
|
||||
<prompt>
|
||||
{{{user_prompt}}}
|
||||
</prompt>
|
||||
|
||||
Match the indentation in the original file in the inserted {{content_type}}, don't include any indentation on blank lines.
|
||||
|
||||
Immediately start with the following format with no remarks:
|
||||
|
||||
```
|
||||
\{{INSERTED_CODE}}
|
||||
```
|
||||
{{else}}
|
||||
Edit the section of {{content_type}} in <rewrite_this></rewrite_this> tags based on the following prompt:
|
||||
|
||||
<prompt>
|
||||
{{{user_prompt}}}
|
||||
</prompt>
|
||||
|
||||
{{#if rewrite_section}}
|
||||
And here's the section to rewrite based on that prompt again for reference:
|
||||
<instructions>
|
||||
{{#if has_insertion}}
|
||||
Insert text anywhere you see marked with <insert_here></insert_here> tags. It's CRITICAL that you DO NOT include <insert_here> tags in your output.
|
||||
{{/if}}
|
||||
{{#if has_replacement}}
|
||||
Edit text that you see surrounded with <edit_here>...</edit_here> tags. It's CRITICAL that you DO NOT include <edit_here> tags in your output.
|
||||
{{/if}}
|
||||
Make no changes to the rewritten content outside these tags.
|
||||
|
||||
<snippet language="{{ language_name }}" annotated="true">
|
||||
{{{ rewrite_section_prefix }}}
|
||||
<rewrite_this>
|
||||
{{{rewrite_section}}}
|
||||
{{{ rewrite_section_with_edits }}}
|
||||
</rewrite_this>
|
||||
{{/if}}
|
||||
{{{ rewrite_section_suffix }}}
|
||||
</snippet>
|
||||
|
||||
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
|
||||
Rewrite the lines enclosed within the <rewrite_this></rewrite_this> tags in accordance with the provided instructions and the prompt below.
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}. Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.
|
||||
<prompt>
|
||||
{{{ user_prompt }}}
|
||||
</prompt>
|
||||
|
||||
Do not include <insert_here> or <edit_here> annotations in your output. Here is a clean copy of the snippet without annotations for your reference.
|
||||
|
||||
<snippet>
|
||||
{{{ rewrite_section_prefix }}}
|
||||
{{{ rewrite_section }}}
|
||||
{{{ rewrite_section_suffix }}}
|
||||
</snippet>
|
||||
</instructions>
|
||||
|
||||
<guidelines_reminder>
|
||||
1. Focus on necessary changes: Modify only what's required to fulfill the prompt.
|
||||
2. Preserve context: Maintain all surrounding content as-is, ensuring the rewritten section seamlessly integrates with the existing document structure and flow.
|
||||
3. Exclude annotation tags: Do not output <rewrite_this>, </rewrite_this>, <edit_here>, or <insert_here> tags.
|
||||
4. Maintain indentation: Begin at the original file's indentation level.
|
||||
5. Complete rewrite: Continue until the entire section is rewritten, even if no further changes are needed.
|
||||
6. Avoid elisions: Always write out the full section without unnecessary omissions. NEVER say `// ...` or `// ...existing code` in your output.
|
||||
7. Respect content boundaries: Preserve code integrity.
|
||||
</guidelines_reminder>
|
||||
|
||||
Immediately start with the following format with no remarks:
|
||||
|
||||
```
|
||||
\{{REWRITTEN_CODE}}
|
||||
```
|
||||
{{/if}}
|
||||
|
||||
@@ -1,312 +1,301 @@
|
||||
<task_description>
|
||||
<workflow>
|
||||
Guide the user through code changes in numbered steps where each step focuses on a single individual function, type definition, etc.
|
||||
Surround each distinct step in a <step></step> XML tag. The user will be performing these steps in a code editor
|
||||
named Zed, which is where they will have entered this prompt and will be seeing the response.
|
||||
|
||||
# Code Change Workflow
|
||||
<instructions>
|
||||
- Use the language of the file for code fence blocks unless otherwise specified.
|
||||
- Include a code or file action in each step.
|
||||
- Provide error handling and input validation where appropriate.
|
||||
- Adapt explanations based on the user's perceived level of expertise.
|
||||
- Include comments in code examples to enhance understanding.
|
||||
- Consider more complex programming scenarios when relevant.
|
||||
- Avoid using the terminal to perform filesystem operations such as creating, deleting, renaming, and editing files. Assume the user prefers to use Zed for these operations, unless the prompt suggests that the user is expecting terminal commands.
|
||||
- When creating a new file, Zed will automatically create any necessary directories in that file's path. So when a step will create a new file in Zed, don't mention needing to create the directory that it will go in.
|
||||
- Assume the user is performing these actions in the Zed code editor, so avoid redundancies like "In your code editor, ..." or "In Zed, ..."
|
||||
- Be concise without leaving out important information.
|
||||
</instructions>
|
||||
|
||||
Your task is to guide the user through code changes using a series of steps. Each step should describe a high-level change, which can consist of multiple edits to distinct locations in the codebase.
|
||||
Examples of user-assistant dialogs with step numbers and user follow-up requests:
|
||||
|
||||
## Output Example
|
||||
|
||||
Provide output as XML, with the following format:
|
||||
|
||||
<step>
|
||||
Update the Person struct to store an age
|
||||
<example language="rust">
|
||||
<turn number="1">
|
||||
<user>Create a Library struct with a vector of Book structs. Include methods to add books and find books by author. Use generics for the book ID.</user>
|
||||
<assistant>Certainly! Let's create a Library struct with a vector of Book structs, including methods to add books and find books by author, using generics for the book ID:
|
||||
|
||||
<step>Define the Book and Library structs
|
||||
```rust
|
||||
struct Person {
|
||||
// existing fields...
|
||||
age: u8,
|
||||
height: f32,
|
||||
// existing fields...
|
||||
use std::fmt::Display;
|
||||
|
||||
struct Book<T>
|
||||
where
|
||||
T: Display + Clone
|
||||
{
|
||||
id: T,
|
||||
title: String,
|
||||
author: String,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn age(&self) -> u8 {
|
||||
self.age
|
||||
struct Library<T>
|
||||
where
|
||||
T: Display + Clone
|
||||
{
|
||||
books: Vec<Book<T>>,
|
||||
}
|
||||
```
|
||||
</step>
|
||||
<step>Implement methods for the Library struct
|
||||
```rust
|
||||
impl<T> Library<T>
|
||||
where
|
||||
T: Display + Clone
|
||||
{
|
||||
fn new() -> Self {
|
||||
Library { books: Vec::new() }
|
||||
}
|
||||
|
||||
fn add_book(&mut self, id: T, title: String, author: String) {
|
||||
let book = Book { id, title, author };
|
||||
self.books.push(book);
|
||||
}
|
||||
|
||||
fn find_books_by_author(&self, author: &str) -> Vec<&Book<T>> {
|
||||
self.books
|
||||
.iter()
|
||||
.filter(|book| book.author.to_lowercase() == author.to_lowercase())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/person.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>height: f32,</search>
|
||||
<description>Add the age field</description>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/person.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>impl Person {</search>
|
||||
<description>Add the age getter</description>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
## Output Format
|
||||
This code defines:
|
||||
1. A generic Book struct with an ID of type T, title, and author
|
||||
2. A Library struct containing a vector of Books
|
||||
3. Methods to create a new Library, add books, and find books by author
|
||||
The use of generics allows flexibility in the type of ID used for books.</assistant>
|
||||
</turn>
|
||||
|
||||
First, each `<step>` must contain a written description of the change that should be made. The description should begin with a high-level overview, and can contain markdown code blocks as well. The description should be self-contained and actionable.
|
||||
|
||||
After the description, each `<step>` must contain one or more `<edit>` tags, each of which refer to a specific range in a source file. Each `<edit>` tag must contain the following child tags:
|
||||
|
||||
### `<path>` (required)
|
||||
|
||||
This tag contains the path to the file that will be changed. It can be an existing path, or a path that should be created.
|
||||
|
||||
### `<search>` (optional)
|
||||
|
||||
This tag contains a search string to locate in the source file, e.g. `pub fn baz() {`. If not provided, the new content will be inserted at the top of the file. Make sure to produce a string that exists in the source file and that isn't ambiguous. When there's ambiguity, add more lines to the search to eliminate it.
|
||||
|
||||
### `<description>` (required)
|
||||
|
||||
This tag contains a single-line description of the edit that should be made at the given location.
|
||||
|
||||
### `<operation>` (required)
|
||||
|
||||
This tag indicates what type of change should be made, relative to the given location. It can be one of the following:
|
||||
- `update`: Rewrites the specified string entirely based on the given description.
|
||||
- `create`: Creates a new file with the given path based on the provided description.
|
||||
- `insert_before`: Inserts new text based on the given description before the specified search string.
|
||||
- `insert_after`: Inserts new text based on the given description after the specified search string.
|
||||
- `delete`: Deletes the specified string from the containing file.
|
||||
|
||||
<guidelines>
|
||||
- There's no need to describe *what* to do, just *where* to do it.
|
||||
- Only reference locations that actually exist (unless you're creating a file).
|
||||
- If creating a file, assume any subsequent updates are included at the time of creation.
|
||||
- Don't create and then update a file. Always create new files in one hot.
|
||||
- Prefer multiple edits to smaller regions, as opposed to one big edit to a larger region.
|
||||
- Don't produce edits that intersect each other. In that case, merge them into a bigger edit.
|
||||
- Never nest an edit with another edit. Never include CDATA. All edits are leaf nodes.
|
||||
- Descriptions are required for all edits except delete.
|
||||
- When generating multiple edits, ensure the descriptions are specific to each individual operation.
|
||||
- Avoid referring to the search string in the description. Focus on the change to be made, not the location where it's made. That's implicit with the `search` string you provide.
|
||||
- Don't generate multiple edits at the same location. Instead, combine them together in a single edit with a succinct combined description.
|
||||
- Always ensure imports are added if you're referencing symbols that are not in scope.
|
||||
</guidelines>
|
||||
|
||||
Here are some concrete examples.
|
||||
|
||||
<example>
|
||||
<message role="user">
|
||||
|
||||
```rs src/shapes.rs
|
||||
pub mod rectangle;
|
||||
pub mod circle;
|
||||
```
|
||||
|
||||
```rs src/shapes/rectangle.rs
|
||||
pub struct Rectangle {
|
||||
width: f64,
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
pub fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```rs src/shapes/circle.rs
|
||||
pub struct Circle {
|
||||
radius: f64,
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
pub fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update all shapes to store their origin as an (x, y) tuple and implement Display.
|
||||
</message>
|
||||
|
||||
<message role="assistant">
|
||||
We'll need to update both the rectangle and circle modules.
|
||||
|
||||
<step>
|
||||
Add origin fields to both shape types.
|
||||
<turn number="2">
|
||||
<user>How can I use this Library struct in a main function?</user>
|
||||
<assistant>Here's how you can use the Library struct in a main function:
|
||||
|
||||
<step>Create a main function to demonstrate Library usage
|
||||
```rust
|
||||
struct Rectangle {
|
||||
// existing fields ...
|
||||
origin: (f64, f64),
|
||||
fn main() {
|
||||
// Create a new Library with string IDs
|
||||
let mut library = Library::new();
|
||||
|
||||
// Add some books
|
||||
library.add_book(String::from("001"), String::from("The Rust Programming Language"), String::from("Steve Klabnik"));
|
||||
library.add_book(String::from("002"), String::from("Programming Rust"), String::from("Jim Blandy"));
|
||||
library.add_book(String::from("003"), String::from("Rust in Action"), String::from("Tim McNamara"));
|
||||
|
||||
// Find books by author
|
||||
let author = "Steve Klabnik";
|
||||
let books = library.find_books_by_author(author);
|
||||
|
||||
println!("Books by {}:", author);
|
||||
for book in books {
|
||||
println!("ID: {}, Title: {}", book.id, book.title);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
struct Circle {
|
||||
// existing fields ...
|
||||
origin: (f64, f64),
|
||||
}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>
|
||||
width: f64,
|
||||
height: f64,
|
||||
</search>
|
||||
<description>Add the origin field to Rectangle</description>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>
|
||||
radius: f64,
|
||||
</search>
|
||||
<description>Add the origin field to Circle</description>
|
||||
</edit>
|
||||
|
||||
<step>
|
||||
Update both shape's constructors to take an origin.
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>update</operation>
|
||||
<search>
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
</search>
|
||||
<description>Update the Rectangle new function to take an origin</description>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>update</operation>
|
||||
<search>
|
||||
fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
</search>
|
||||
<description>Update the Circle new function to take an origin</description>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
<step>
|
||||
Implement Display for both shapes
|
||||
This main function demonstrates:
|
||||
1. Creating a new Library instance
|
||||
2. Adding books with string IDs
|
||||
3. Finding and displaying books by a specific author</assistant>
|
||||
</turn>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>
|
||||
struct Rectangle {
|
||||
</search>
|
||||
<description>Add an import for the `std::fmt` module</description>
|
||||
</edit>
|
||||
<turn number="3">
|
||||
<user>Can you add a method to remove a book by ID and handle potential errors?</user>
|
||||
<assistant>Certainly! Let's add a method to remove a book by ID and handle potential errors:
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>
|
||||
Rectangle { width, height }
|
||||
<step>Add a remove_book method to the Library struct
|
||||
```rust
|
||||
use std::fmt::Display;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BookNotFoundError;
|
||||
|
||||
impl Error for BookNotFoundError {}
|
||||
|
||||
impl fmt::Display for BookNotFoundError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Book not found")
|
||||
}
|
||||
}
|
||||
</search>
|
||||
<description>Add a Display implementation for Rectangle</description>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>
|
||||
struct Circle {
|
||||
</search>
|
||||
<description>Add an import for the `std::fmt` module</description>
|
||||
</edit>
|
||||
impl<T> Library<T>
|
||||
where
|
||||
T: Display + Clone + PartialEq
|
||||
{
|
||||
// ... (previous methods remain the same)
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>
|
||||
Circle { radius }
|
||||
fn remove_book(&mut self, id: &T) -> Result<Book<T>, BookNotFoundError> {
|
||||
if let Some(index) = self.books.iter().position(|book| &book.id == id) {
|
||||
Ok(self.books.remove(index))
|
||||
} else {
|
||||
Err(BookNotFoundError)
|
||||
}
|
||||
}
|
||||
}
|
||||
</search>
|
||||
<description>Add a Display implementation for Circle</description>
|
||||
</edit>
|
||||
```
|
||||
</step>
|
||||
</message>
|
||||
<step>Update the main function to demonstrate book removal
|
||||
```rust
|
||||
fn main() {
|
||||
// ... (previous code remains the same)
|
||||
|
||||
// Attempt to remove a book
|
||||
let id_to_remove = String::from("002");
|
||||
match library.remove_book(&id_to_remove) {
|
||||
Ok(removed_book) => println!("Removed book: {} by {}", removed_book.title, removed_book.author),
|
||||
Err(e) => println!("Error removing book: {}", e),
|
||||
}
|
||||
|
||||
// Attempt to remove a non-existent book
|
||||
let non_existent_id = String::from("999");
|
||||
match library.remove_book(&non_existent_id) {
|
||||
Ok(_) => println!("Book removed successfully"),
|
||||
Err(e) => println!("Error removing book: {}", e),
|
||||
}
|
||||
|
||||
// Display remaining books
|
||||
println!("\nRemaining books:");
|
||||
for book in &library.books {
|
||||
println!("ID: {}, Title: {} by {}", book.id, book.title, book.author);
|
||||
}
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
This update includes:
|
||||
1. A new remove_book method that returns a Result type
|
||||
2. A custom BookNotFoundError for error handling
|
||||
3. Demonstration of successful book removal and error handling for non-existent books
|
||||
4. Display of remaining books after removal attempts
|
||||
|
||||
The remove_book method uses the PartialEq trait to compare book IDs, allowing for flexible ID types while maintaining type safety.</assistant>
|
||||
</turn>
|
||||
</example>
|
||||
|
||||
<example number=2>
|
||||
<message role="user">
|
||||
<example language="go">
|
||||
<turn number="1">
|
||||
<user>Create a Person struct with name, age, and email fields, and a method to greet. Include error handling for age.</user>
|
||||
<assistant>Certainly! Let's create a Person struct with the specified fields and a method to greet in Go:
|
||||
|
||||
```rs src/user.rs
|
||||
struct User {
|
||||
pub name: String,
|
||||
age: u32,
|
||||
email: String,
|
||||
<step>Define the Person struct and NewPerson function with error handling
|
||||
```go
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
func NewPerson(name string, age int, email string) (*Person, error) {
|
||||
if age < 0 || age > 150 {
|
||||
return nil, fmt.Errorf("invalid age: %d", age)
|
||||
}
|
||||
return &Person{Name: name, Age: age, Email: email}, nil
|
||||
}
|
||||
```
|
||||
</step>
|
||||
<step>Implement the Greet method for the Person struct
|
||||
```go
|
||||
func (p *Person) Greet() string {
|
||||
return fmt.Sprintf("Hello, I'm %s, %d years old. You can reach me at %s.", p.Name, p.Age, p.Email)
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
This code defines a Person struct, a NewPerson function with age validation, and a Greet method.</assistant>
|
||||
</turn>
|
||||
|
||||
<turn number="2">
|
||||
<user>How can I use these in a main function?</user>
|
||||
<assistant>Here's how to use the Person struct and its methods in a main function:
|
||||
|
||||
<step>Create a main function with error handling
|
||||
```go
|
||||
func main() {
|
||||
// Create a new Person instance
|
||||
person, err := NewPerson("Alice", 30, "alice@example.com")
|
||||
if err != nil {
|
||||
fmt.Println("Error creating person:", err)
|
||||
return
|
||||
}
|
||||
|
||||
pub fn print_info(&self) {
|
||||
todo!()
|
||||
// Use the Greet method
|
||||
fmt.Println(person.Greet())
|
||||
|
||||
// Demonstrate error handling with invalid age
|
||||
invalidPerson, err := NewPerson("Bob", 200, "bob@example.com")
|
||||
if err != nil {
|
||||
fmt.Println("Error creating person with invalid age:", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
Let's print all the user information and delete the email field.
|
||||
</message>
|
||||
This main function demonstrates:
|
||||
1. Creating a valid Person instance
|
||||
2. Using the Greet method
|
||||
3. Handling errors when creating a Person with an invalid age</assistant>
|
||||
</turn>
|
||||
|
||||
<message role="assistant">
|
||||
<step>
|
||||
Update the 'print_info' method to print user information
|
||||
<turn number="3">
|
||||
<user>Can you add a method to update the email with validation?</user>
|
||||
<assistant>Certainly! Let's add a method to update the email with validation:
|
||||
|
||||
```rust
|
||||
impl User {
|
||||
// ... other methods ...
|
||||
<step>Add UpdateEmail method with validation
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
pub fn print_info(&self) {
|
||||
println!("Name: {name}, Age: {age}", name = self.name, age = self.age);
|
||||
func (p *Person) UpdateEmail(newEmail string) error {
|
||||
// Simple email validation using regex
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
if !emailRegex.MatchString(newEmail) {
|
||||
return fmt.Errorf("invalid email format: %s", newEmail)
|
||||
}
|
||||
p.Email = newEmail
|
||||
return nil
|
||||
}
|
||||
```
|
||||
</step>
|
||||
<step>Update the main function to demonstrate email update
|
||||
```go
|
||||
func main() {
|
||||
// ... (previous code remains the same)
|
||||
|
||||
// Demonstrate email update
|
||||
err = person.UpdateEmail("alice.new@example.com")
|
||||
if err != nil {
|
||||
fmt.Println("Error updating email:", err)
|
||||
} else {
|
||||
fmt.Println("Updated greeting:", person.Greet())
|
||||
}
|
||||
|
||||
// Demonstrate email update with invalid email
|
||||
err = person.UpdateEmail("invalid-email")
|
||||
if err != nil {
|
||||
fmt.Println("Error updating email:", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/user.rs</path>
|
||||
<operation>update</operation>
|
||||
<search>
|
||||
pub fn print_info(&self) {
|
||||
todo!()
|
||||
}
|
||||
</search>
|
||||
<description>Print all the user information</description>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
<step>
|
||||
Remove the 'email' field from the User struct
|
||||
|
||||
<edit>
|
||||
<path>src/user.rs</path>
|
||||
<operation>delete</operation>
|
||||
<search>
|
||||
email: String,
|
||||
</search>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/user.rs</path>
|
||||
<operation>update</operation>
|
||||
<symbol>
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
}
|
||||
</symbol>
|
||||
<description>Remove email parameter from new method</description>
|
||||
</edit>
|
||||
</step>
|
||||
</message>
|
||||
This update includes:
|
||||
1. An UpdateEmail method with email format validation
|
||||
2. Demonstration of successful email update in the main function
|
||||
3. Handling of invalid email update attempt</assistant>
|
||||
</turn>
|
||||
</example>
|
||||
|
||||
You should think step by step. When possible, produce smaller, coherent logical steps as opposed to one big step that combines lots of heterogeneous edits.
|
||||
|
||||
</task_description>
|
||||
</workflow>
|
||||
|
||||
@@ -15,7 +15,6 @@ With each location, you will produce a brief, one-line description of the change
|
||||
- When generating multiple suggestions, ensure the descriptions are specific to each individual operation.
|
||||
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
|
||||
- Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description.
|
||||
- To add imports respond with a suggestion where the `"symbol"` key is set to `"#imports"`
|
||||
</guidelines>
|
||||
</overview>
|
||||
|
||||
@@ -204,7 +203,6 @@ Add a 'use std::fmt;' statement at the beginning of the file
|
||||
{
|
||||
"kind": "PrependChild",
|
||||
"path": "src/vehicle.rs",
|
||||
"symbol": "#imports",
|
||||
"description": "Add 'use std::fmt' statement"
|
||||
}
|
||||
]
|
||||
@@ -415,7 +413,6 @@ Add a 'load_from_file' method to Config and import necessary modules
|
||||
{
|
||||
"kind": "PrependChild",
|
||||
"path": "src/config.rs",
|
||||
"symbol": "#imports",
|
||||
"description": "Import std::fs and std::io modules"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"buffer_font_family": "Zed Plex Mono",
|
||||
// Set the buffer text's font fallbacks, this will be merged with
|
||||
// the platform's default fallbacks.
|
||||
"buffer_font_fallbacks": null,
|
||||
"buffer_font_fallbacks": [],
|
||||
// The OpenType features to enable for text in the editor.
|
||||
"buffer_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -54,7 +54,7 @@
|
||||
"ui_font_family": "Zed Plex Sans",
|
||||
// Set the UI's font fallbacks, this will be merged with the platform's
|
||||
// default font fallbacks.
|
||||
"ui_font_fallbacks": null,
|
||||
"ui_font_fallbacks": [],
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -64,15 +64,9 @@
|
||||
"ui_font_weight": 400,
|
||||
// The default font size for text in the UI
|
||||
"ui_font_size": 16,
|
||||
// How much to fade out unused code.
|
||||
"unnecessary_code_fade": 0.3,
|
||||
// The factor to grow the active pane by. Defaults to 1.0
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
// The direction that you want to split panes horizontally. Defaults to "up"
|
||||
"pane_split_direction_horizontal": "up",
|
||||
// The direction that you want to split panes horizontally. Defaults to "left"
|
||||
"pane_split_direction_vertical": "left",
|
||||
// Centered layout related settings.
|
||||
"centered_layout": {
|
||||
// The relative width of the left padding of the central pane from the
|
||||
@@ -227,8 +221,6 @@
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
},
|
||||
// Enable middle-click paste on Linux.
|
||||
"middle_click_paste": true,
|
||||
// What to do when multibuffer is double clicked in some of its excerpts
|
||||
// (parts of singleton buffers).
|
||||
// May take 2 values:
|
||||
@@ -279,13 +271,6 @@
|
||||
"relative_line_numbers": false,
|
||||
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
||||
"search_wrap": true,
|
||||
// Search options to enable by default when opening new project and buffer searches.
|
||||
"search": {
|
||||
"whole_word": false,
|
||||
"case_sensitive": false,
|
||||
"include_ignored": false,
|
||||
"regex": false
|
||||
},
|
||||
// When to populate a new search's query based on the text under the cursor.
|
||||
// This setting can take the following three values:
|
||||
//
|
||||
@@ -296,7 +281,6 @@
|
||||
// 3. Never populate the search query
|
||||
// "never"
|
||||
"seed_search_query_from_cursor": "always",
|
||||
"use_smartcase_search": false,
|
||||
// Inlay hint related settings
|
||||
"inlay_hints": {
|
||||
// Global switch to toggle hints on and off, switched off by default.
|
||||
@@ -411,9 +395,9 @@
|
||||
// The default model to use when creating new contexts.
|
||||
"default_model": {
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
"provider": "openai",
|
||||
// The model to use.
|
||||
"model": "claude-3-5-sonnet"
|
||||
"model": "gpt-4o"
|
||||
}
|
||||
},
|
||||
// The settings for slash commands.
|
||||
@@ -520,8 +504,6 @@
|
||||
// "soft_wrap": "editor_width",
|
||||
// 4. Soft wrap lines at the preferred line length.
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
// 5. Soft wrap lines at the preferred line length or the editor width (whichever is smaller).
|
||||
// "soft_wrap": "bounded",
|
||||
"soft_wrap": "prefer_line",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
@@ -611,7 +593,7 @@
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "args": ["--login"]
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
@@ -740,13 +722,7 @@
|
||||
//
|
||||
"file_types": {
|
||||
"JSON": ["flake.lock"],
|
||||
"JSONC": [
|
||||
"**/.zed/**/*.json",
|
||||
"**/zed/**/*.json",
|
||||
"**/Zed/**/*.json",
|
||||
"tsconfig.json",
|
||||
"pyrightconfig.json"
|
||||
]
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "tsconfig.json"]
|
||||
},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
//
|
||||
@@ -860,7 +836,6 @@
|
||||
"language_servers": ["starpls", "!buck2-lsp", "..."]
|
||||
},
|
||||
"Svelte": {
|
||||
"language_servers": ["svelte-language-server", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-svelte"]
|
||||
@@ -884,7 +859,6 @@
|
||||
}
|
||||
},
|
||||
"Vue.js": {
|
||||
"language_servers": ["vue-language-server", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -911,8 +885,7 @@
|
||||
"api_url": "https://generativelanguage.googleapis.com"
|
||||
},
|
||||
"ollama": {
|
||||
"api_url": "http://localhost:11434",
|
||||
"low_speed_timeout_in_seconds": 60
|
||||
"api_url": "http://localhost:11434"
|
||||
},
|
||||
"openai": {
|
||||
"version": "1",
|
||||
@@ -962,7 +935,6 @@
|
||||
},
|
||||
// Vim settings
|
||||
"vim": {
|
||||
"toggle_relative_line_numbers": false,
|
||||
"use_system_clipboard": "always",
|
||||
"use_multiline_find": false,
|
||||
"use_smartcase_find": false,
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "args": ["--login"]
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "Andromeda",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "Atelier",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "Ayu",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "Gruvbox",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "One",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "Rosé Pine",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "Sandcastle",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "Solarized",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"name": "Summercamp",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
allow-private-module-inception = true
|
||||
26
compose.yml
@@ -33,31 +33,5 @@ services:
|
||||
volumes:
|
||||
- ./livekit.yaml:/livekit.yaml
|
||||
|
||||
postgrest_app:
|
||||
image: postgrest/postgrest
|
||||
container_name: postgrest_app
|
||||
ports:
|
||||
- 8081:8081
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://postgres@postgres:5432/zed
|
||||
volumes:
|
||||
- ./crates/collab/postgrest_app.conf:/etc/postgrest.conf
|
||||
command: postgrest /etc/postgrest.conf
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
postgrest_llm:
|
||||
image: postgrest/postgrest
|
||||
container_name: postgrest_llm
|
||||
ports:
|
||||
- 8082:8082
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://postgres@postgres:5432/zed_llm
|
||||
volumes:
|
||||
- ./crates/collab/postgrest_llm.conf:/etc/postgrest.conf
|
||||
command: postgrest /etc/postgrest.conf
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
@@ -3,9 +3,10 @@ use editor::Editor;
|
||||
use extension::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
|
||||
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
|
||||
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,
|
||||
@@ -13,7 +14,7 @@ use language::{
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
|
||||
use ui::{prelude::*, ContextMenu};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
@@ -26,7 +27,7 @@ pub struct ActivityIndicator {
|
||||
statuses: Vec<LspStatus>,
|
||||
project: Model<Project>,
|
||||
auto_updater: Option<Model<AutoUpdater>>,
|
||||
context_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
context_menu: Option<View<ContextMenu>>,
|
||||
}
|
||||
|
||||
struct LspStatus {
|
||||
@@ -40,6 +41,7 @@ struct PendingWork<'a> {
|
||||
progress: &'a LanguageServerProgress,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Content {
|
||||
icon: Option<gpui::AnyElement>,
|
||||
message: String,
|
||||
@@ -77,7 +79,7 @@ impl ActivityIndicator {
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
auto_updater,
|
||||
context_menu_handle: Default::default(),
|
||||
context_menu: None,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -150,7 +152,7 @@ impl ActivityIndicator {
|
||||
) -> impl Iterator<Item = PendingWork<'a>> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.language_server_statuses(cx)
|
||||
.language_server_statuses()
|
||||
.rev()
|
||||
.filter_map(|(server_id, status)| {
|
||||
if status.pending_work.is_empty() {
|
||||
@@ -172,7 +174,7 @@ impl ActivityIndicator {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Option<Content> {
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
@@ -201,7 +203,7 @@ impl ActivityIndicator {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return Some(Content {
|
||||
return Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
@@ -214,7 +216,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message,
|
||||
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Show any language server installation info.
|
||||
@@ -233,7 +235,7 @@ impl ActivityIndicator {
|
||||
}
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return Some(Content {
|
||||
return Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
@@ -241,11 +243,11 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: format!("Downloading {}...", downloading.join(", "),),
|
||||
on_click: None,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if !checking_for_update.is_empty() {
|
||||
return Some(Content {
|
||||
return Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
@@ -256,11 +258,11 @@ impl ActivityIndicator {
|
||||
checking_for_update.join(", "),
|
||||
),
|
||||
on_click: None,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
return Some(Content {
|
||||
return Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
@@ -273,12 +275,12 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.show_error_message(&Default::default(), cx)
|
||||
})),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Show any formatting failure
|
||||
if let Some(failure) = self.project.read(cx).last_formatting_failure() {
|
||||
return Some(Content {
|
||||
return Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
@@ -288,13 +290,13 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
})),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Some(Content {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
@@ -302,8 +304,8 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: None,
|
||||
}),
|
||||
AutoUpdateStatus::Downloading => Some(Content {
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
@@ -311,8 +313,8 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: None,
|
||||
}),
|
||||
AutoUpdateStatus::Installing => Some(Content {
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
@@ -320,8 +322,8 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: None,
|
||||
}),
|
||||
AutoUpdateStatus::Updated { binary_path } => Some(Content {
|
||||
},
|
||||
AutoUpdateStatus::Updated { binary_path } => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new({
|
||||
@@ -330,8 +332,8 @@ impl ActivityIndicator {
|
||||
};
|
||||
move |_, cx| workspace::reload(&reload, cx)
|
||||
})),
|
||||
}),
|
||||
AutoUpdateStatus::Errored => Some(Content {
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
@@ -341,8 +343,8 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&Default::default(), cx)
|
||||
})),
|
||||
}),
|
||||
AutoUpdateStatus::Idle => None,
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -350,7 +352,7 @@ impl ActivityIndicator {
|
||||
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
|
||||
{
|
||||
if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
|
||||
return Some(Content {
|
||||
return Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
@@ -358,15 +360,80 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: format!("Updating {extension_id} extension…"),
|
||||
on_click: None,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.context_menu_handle.toggle(cx);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,88 +441,33 @@ impl EventEmitter<Event> for ActivityIndicator {}
|
||||
|
||||
impl Render for ActivityIndicator {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let result = h_flex()
|
||||
let content = self.content_to_render(cx);
|
||||
|
||||
let mut result = h_flex()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
.on_action(cx.listener(Self::dismiss_error_message));
|
||||
let Some(content) = self.content_to_render(cx) else {
|
||||
return result;
|
||||
};
|
||||
let this = cx.view().downgrade();
|
||||
result.gap_2().child(
|
||||
PopoverMenu::new("activity-indicator-popover")
|
||||
.trigger(
|
||||
ButtonLike::new("activity-indicator-trigger").child(
|
||||
h_flex()
|
||||
.id("activity-indicator-status")
|
||||
.gap_2()
|
||||
.children(content.icon)
|
||||
.child(Label::new(content.message).size(LabelSize::Small))
|
||||
.when_some(content.on_click, |this, handler| {
|
||||
this.on_click(cx.listener(move |this, _, cx| {
|
||||
handler(this, cx);
|
||||
}))
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
}),
|
||||
),
|
||||
|
||||
if let Some(on_click) = content.on_click {
|
||||
result = result
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
on_click(this, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
result
|
||||
.gap_2()
|
||||
.children(content.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()),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.menu(move |cx| {
|
||||
let strong_this = this.upgrade()?;
|
||||
let mut has_work = false;
|
||||
let menu = ContextMenu::build(cx, |mut menu, cx| {
|
||||
for work in strong_this.read(cx).pending_language_server_work(cx) {
|
||||
has_work = true;
|
||||
let this = this.clone();
|
||||
let mut title = work
|
||||
.progress
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or(work.progress_token)
|
||||
.to_owned();
|
||||
|
||||
if work.progress.is_cancellable {
|
||||
let language_server_id = work.language_server_id;
|
||||
let token = work.progress_token.to_string();
|
||||
let title = SharedString::from(title);
|
||||
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_handle.hide(cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if let Some(progress_message) = work.progress.message.as_ref() {
|
||||
title.push_str(": ");
|
||||
title.push_str(progress_message);
|
||||
}
|
||||
|
||||
menu = menu.label(title);
|
||||
}
|
||||
}
|
||||
menu
|
||||
});
|
||||
has_work.then_some(menu)
|
||||
}),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ path = "src/anthropic.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
futures.workspace = true
|
||||
http_client.workspace = true
|
||||
isahc.workspace = true
|
||||
@@ -26,7 +25,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
mod supported_countries;
|
||||
|
||||
use std::time::Duration;
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use isahc::config::Configurable;
|
||||
use isahc::http::{HeaderMap, HeaderValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub use supported_countries::*;
|
||||
|
||||
pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
|
||||
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)]
|
||||
@@ -42,8 +38,6 @@ pub enum Model {
|
||||
Custom {
|
||||
name: String,
|
||||
max_tokens: usize,
|
||||
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
|
||||
display_name: Option<String>,
|
||||
/// Override this model with a different Anthropic model for tool calls.
|
||||
tool_override: Option<String>,
|
||||
/// Indicates whether this custom model supports caching.
|
||||
@@ -83,9 +77,7 @@ impl Model {
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
} => display_name.as_ref().unwrap_or(name),
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,66 +191,6 @@ pub async fn stream_completion(
|
||||
request: Request,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request, low_speed_timeout)
|
||||
.await
|
||||
.map(|output| output.0)
|
||||
}
|
||||
|
||||
/// https://docs.anthropic.com/en/api/rate-limits#response-headers
|
||||
#[derive(Debug)]
|
||||
pub struct RateLimitInfo {
|
||||
pub requests_limit: usize,
|
||||
pub requests_remaining: usize,
|
||||
pub requests_reset: DateTime<Utc>,
|
||||
pub tokens_limit: usize,
|
||||
pub tokens_remaining: usize,
|
||||
pub tokens_reset: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl RateLimitInfo {
|
||||
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Result<Self> {
|
||||
let tokens_limit = get_header("anthropic-ratelimit-tokens-limit", headers)?.parse()?;
|
||||
let requests_limit = get_header("anthropic-ratelimit-requests-limit", headers)?.parse()?;
|
||||
let tokens_remaining =
|
||||
get_header("anthropic-ratelimit-tokens-remaining", headers)?.parse()?;
|
||||
let requests_remaining =
|
||||
get_header("anthropic-ratelimit-requests-remaining", headers)?.parse()?;
|
||||
let requests_reset = get_header("anthropic-ratelimit-requests-reset", headers)?;
|
||||
let tokens_reset = get_header("anthropic-ratelimit-tokens-reset", headers)?;
|
||||
let requests_reset = DateTime::parse_from_rfc3339(requests_reset)?.to_utc();
|
||||
let tokens_reset = DateTime::parse_from_rfc3339(tokens_reset)?.to_utc();
|
||||
|
||||
Ok(Self {
|
||||
requests_limit,
|
||||
tokens_limit,
|
||||
requests_remaining,
|
||||
tokens_remaining,
|
||||
requests_reset,
|
||||
tokens_reset,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
|
||||
Ok(headers
|
||||
.get(key)
|
||||
.ok_or_else(|| anyhow!("missing header `{key}`"))?
|
||||
.to_str()?)
|
||||
}
|
||||
|
||||
pub async fn stream_completion_with_rate_limit_info(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<
|
||||
(
|
||||
BoxStream<'static, Result<Event, AnthropicError>>,
|
||||
Option<RateLimitInfo>,
|
||||
),
|
||||
AnthropicError,
|
||||
> {
|
||||
let request = StreamingRequest {
|
||||
base: request,
|
||||
stream: true,
|
||||
@@ -288,9 +220,8 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
.await
|
||||
.context("failed to send request to Anthropic")?;
|
||||
if response.status().is_success() {
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
let reader = BufReader::new(response.into_body());
|
||||
let stream = reader
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(|line| async move {
|
||||
match line {
|
||||
@@ -304,8 +235,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok((stream, rate_limits.log_err()))
|
||||
.boxed())
|
||||
} else {
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
@@ -331,6 +261,28 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_text_from_events(
|
||||
response: impl Stream<Item = Result<Event, AnthropicError>>,
|
||||
) -> impl Stream<Item = Result<String, AnthropicError>> {
|
||||
response.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(response) => match response {
|
||||
Event::ContentBlockStart { content_block, .. } => match content_block {
|
||||
Content::Text { text, .. } => Some(Ok(text)),
|
||||
_ => None,
|
||||
},
|
||||
Event::ContentBlockDelta { delta, .. } => match delta {
|
||||
ContentDelta::TextDelta { text } => Some(Ok(text)),
|
||||
_ => None,
|
||||
},
|
||||
Event::Error { error } => Some(Err(AnthropicError::ApiError(error))),
|
||||
_ => None,
|
||||
},
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn extract_tool_args_from_events(
|
||||
tool_name: String,
|
||||
mut events: Pin<Box<dyn Send + Stream<Item = Result<Event>>>>,
|
||||
@@ -339,12 +291,14 @@ pub async fn extract_tool_args_from_events(
|
||||
while let Some(event) = events.next().await {
|
||||
if let Event::ContentBlockStart {
|
||||
index,
|
||||
content_block: ResponseContent::ToolUse { name, .. },
|
||||
content_block,
|
||||
} = event?
|
||||
{
|
||||
if name == tool_name {
|
||||
tool_use_index = Some(index);
|
||||
break;
|
||||
if let Content::ToolUse { name, .. } = content_block {
|
||||
if name == tool_name {
|
||||
tool_use_index = Some(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,7 +342,7 @@ pub struct CacheControl {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub role: Role,
|
||||
pub content: Vec<RequestContent>,
|
||||
pub content: Vec<Content>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
@@ -400,7 +354,7 @@ pub enum Role {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum RequestContent {
|
||||
pub enum Content {
|
||||
#[serde(rename = "text")]
|
||||
Text {
|
||||
text: String,
|
||||
@@ -424,26 +378,12 @@ pub enum RequestContent {
|
||||
#[serde(rename = "tool_result")]
|
||||
ToolResult {
|
||||
tool_use_id: String,
|
||||
is_error: bool,
|
||||
content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ResponseContent {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
#[serde(rename = "tool_use")]
|
||||
ToolUse {
|
||||
id: String,
|
||||
name: String,
|
||||
input: serde_json::Value,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ImageSource {
|
||||
#[serde(rename = "type")]
|
||||
@@ -516,7 +456,7 @@ pub struct Response {
|
||||
#[serde(rename = "type")]
|
||||
pub response_type: String,
|
||||
pub role: Role,
|
||||
pub content: Vec<ResponseContent>,
|
||||
pub content: Vec<Content>,
|
||||
pub model: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_reason: Option<String>,
|
||||
@@ -533,7 +473,7 @@ pub enum Event {
|
||||
#[serde(rename = "content_block_start")]
|
||||
ContentBlockStart {
|
||||
index: usize,
|
||||
content_block: ResponseContent,
|
||||
content_block: Content,
|
||||
},
|
||||
#[serde(rename = "content_block_delta")]
|
||||
ContentBlockDelta { index: usize, delta: ContentDelta },
|
||||
@@ -608,6 +548,9 @@ impl ApiError {
|
||||
}
|
||||
|
||||
pub fn is_rate_limit_error(&self) -> bool {
|
||||
matches!(self.error_type.as_str(), "rate_limit_error")
|
||||
match self.error_type.as_str() {
|
||||
"rate_limit_error" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,8 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
vec![
|
||||
"AL", // Albania
|
||||
"DZ", // Algeria
|
||||
"AS", // American Samoa (US)
|
||||
"AD", // Andorra
|
||||
"AO", // Angola
|
||||
"AI", // Anguilla (UK)
|
||||
"AG", // Antigua and Barbuda
|
||||
"AR", // Argentina
|
||||
"AM", // Armenia
|
||||
@@ -32,13 +30,11 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"BE", // Belgium
|
||||
"BZ", // Belize
|
||||
"BJ", // Benin
|
||||
"BM", // Bermuda (UK)
|
||||
"BT", // Bhutan
|
||||
"BO", // Bolivia
|
||||
"BA", // Bosnia and Herzegovina
|
||||
"BW", // Botswana
|
||||
"BR", // Brazil
|
||||
"IO", // British Indian Ocean Territory (UK)
|
||||
"BN", // Brunei
|
||||
"BG", // Bulgaria
|
||||
"BF", // Burkina Faso
|
||||
@@ -47,15 +43,11 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"KH", // Cambodia
|
||||
"CM", // Cameroon
|
||||
"CA", // Canada
|
||||
"KY", // Cayman Islands (UK)
|
||||
"TD", // Chad
|
||||
"CL", // Chile
|
||||
"CX", // Christmas Island (AU)
|
||||
"CC", // Cocos (Keeling) Islands (AU)
|
||||
"CO", // Colombia
|
||||
"KM", // Comoros
|
||||
"CG", // Congo (Brazzaville)
|
||||
"CK", // Cook Islands (NZ)
|
||||
"CR", // Costa Rica
|
||||
"CI", // Côte d'Ivoire
|
||||
"HR", // Croatia
|
||||
@@ -71,28 +63,21 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"GQ", // Equatorial Guinea
|
||||
"EE", // Estonia
|
||||
"SZ", // Eswatini
|
||||
"FK", // Falkland Islands (UK)
|
||||
"FJ", // Fiji
|
||||
"FI", // Finland
|
||||
"FR", // France
|
||||
"GF", // French Guiana (FR)
|
||||
"PF", // French Polynesia (FR)
|
||||
"TF", // French Southern Territories
|
||||
"GA", // Gabon
|
||||
"GM", // Gambia
|
||||
"GE", // Georgia
|
||||
"DE", // Germany
|
||||
"GH", // Ghana
|
||||
"GI", // Gibraltar (UK)
|
||||
"GR", // Greece
|
||||
"GD", // Grenada
|
||||
"GT", // Guatemala
|
||||
"GU", // Guam (US)
|
||||
"GN", // Guinea
|
||||
"GW", // Guinea-Bissau
|
||||
"GY", // Guyana
|
||||
"HT", // Haiti
|
||||
"HM", // Heard Island and McDonald Islands (AU)
|
||||
"HN", // Honduras
|
||||
"HU", // Hungary
|
||||
"IS", // Iceland
|
||||
@@ -131,7 +116,6 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"MD", // Moldova
|
||||
"MC", // Monaco
|
||||
"MN", // Mongolia
|
||||
"MS", // Montserrat (UK)
|
||||
"ME", // Montenegro
|
||||
"MA", // Morocco
|
||||
"MZ", // Mozambique
|
||||
@@ -142,11 +126,8 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"NZ", // New Zealand
|
||||
"NE", // Niger
|
||||
"NG", // Nigeria
|
||||
"NF", // Norfolk Island (AU)
|
||||
"MK", // North Macedonia
|
||||
"MI", // Northern Mariana Islands (UK)
|
||||
"NO", // Norway
|
||||
"NU", // Niue (NZ)
|
||||
"OM", // Oman
|
||||
"PK", // Pakistan
|
||||
"PW", // Palau
|
||||
@@ -156,18 +137,13 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"PY", // Paraguay
|
||||
"PE", // Peru
|
||||
"PH", // Philippines
|
||||
"PN", // Pitcairn (UK)
|
||||
"PL", // Poland
|
||||
"PT", // Portugal
|
||||
"PR", // Puerto Rico (US)
|
||||
"QA", // Qatar
|
||||
"RO", // Romania
|
||||
"RW", // Rwanda
|
||||
"BL", // Saint Barthélemy (FR)
|
||||
"KN", // Saint Kitts and Nevis
|
||||
"LC", // Saint Lucia
|
||||
"MF", // Saint Martin (FR)
|
||||
"PM", // Saint Pierre and Miquelon (FR)
|
||||
"VC", // Saint Vincent and the Grenadines
|
||||
"WS", // Samoa
|
||||
"SM", // San Marino
|
||||
@@ -176,7 +152,6 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"SN", // Senegal
|
||||
"RS", // Serbia
|
||||
"SC", // Seychelles
|
||||
"SH", // Saint Helena, Ascension and Tristan da Cunha (UK)
|
||||
"SL", // Sierra Leone
|
||||
"SG", // Singapore
|
||||
"SK", // Slovakia
|
||||
@@ -195,28 +170,22 @@ static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
"TH", // Thailand
|
||||
"TL", // Timor-Leste
|
||||
"TG", // Togo
|
||||
"TK", // Tokelau (NZ)
|
||||
"TO", // Tonga
|
||||
"TT", // Trinidad and Tobago
|
||||
"TN", // Tunisia
|
||||
"TR", // Türkiye (Turkey)
|
||||
"TM", // Turkmenistan
|
||||
"TC", // Turks and Caicos Islands (UK)
|
||||
"TV", // Tuvalu
|
||||
"UG", // Uganda
|
||||
"UA", // Ukraine (except Crimea, Donetsk, and Luhansk regions)
|
||||
"AE", // United Arab Emirates
|
||||
"GB", // United Kingdom
|
||||
"UM", // United States Minor Outlying Islands (US)
|
||||
"US", // United States of America
|
||||
"UY", // Uruguay
|
||||
"UZ", // Uzbekistan
|
||||
"VU", // Vanuatu
|
||||
"VA", // Vatican City
|
||||
"VN", // Vietnam
|
||||
"VI", // Virgin Islands (US)
|
||||
"VG", // Virgin Islands (UK)
|
||||
"WF", // Wallis and Futuna (FR)
|
||||
"ZM", // Zambia
|
||||
"ZW", // Zimbabwe
|
||||
]
|
||||
|
||||
@@ -25,7 +25,6 @@ anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
cargo_toml.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -33,8 +32,8 @@ client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_servers.workspace = true
|
||||
db.workspace = true
|
||||
context_servers.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
@@ -59,11 +58,9 @@ open_ai = { workspace = true, features = ["schemars"] }
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
@@ -71,10 +68,9 @@ semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smallvec.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
@@ -85,6 +81,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
picker.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
63
crates/assistant/README.zmd
Normal file
@@ -0,0 +1,63 @@
|
||||
> Hello, I'm Nathan Sobo.
|
||||
I'm currently your sole creator and the leader of Zed Industries.
|
||||
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
|
||||
|
||||
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
|
||||
|
||||
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
|
||||
|
||||
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
|
||||
|
||||
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
|
||||
|
||||
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
|
||||
|
||||
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
|
||||
|
||||
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
|
||||
|
||||
@nathan> #zed, can you describe the conversation UI as you currently understand it.
|
||||
|
||||
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
|
||||
|
||||
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
|
||||
|
||||
@nathan> What details around the conversation view are worth exploring?
|
||||
|
||||
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
|
||||
|
||||
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
|
||||
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
|
||||
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
|
||||
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
|
||||
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
|
||||
|
||||
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
|
||||
|
||||
@nathan> @nate, which of these would you like to explore?
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multiple people, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
#zed describe a hypothetical conversation UI that meets the description.
|
||||
|
||||
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
|
||||
|
||||
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
|
||||
|
||||
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
|
||||
|
||||
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
|
||||
|
||||
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
|
||||
|
||||
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
|
||||
|
||||
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
|
||||
|
||||
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
|
||||
|
||||
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.
|
||||
@@ -9,17 +9,15 @@ mod model_selector;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod slash_command;
|
||||
pub(crate) mod slash_command_picker;
|
||||
mod slash_command_picker;
|
||||
pub mod slash_command_settings;
|
||||
mod streaming_diff;
|
||||
mod terminal_inline_assistant;
|
||||
mod tools;
|
||||
mod workflow;
|
||||
|
||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use assistant_tool::ToolRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub use context::*;
|
||||
@@ -27,8 +25,8 @@ use context_servers::ContextServerRegistry;
|
||||
pub use context_store::*;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use gpui::{impl_actions, Context as _};
|
||||
use gpui::Context as _;
|
||||
use gpui::{actions, impl_actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
pub(crate) use inline_assistant::*;
|
||||
use language_model::{
|
||||
@@ -36,7 +34,7 @@ use language_model::{
|
||||
};
|
||||
pub(crate) use model_selector::*;
|
||||
pub use prompts::PromptBuilder;
|
||||
use prompts::PromptLoadingParams;
|
||||
use prompts::PromptOverrideContext;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
@@ -45,7 +43,6 @@ use slash_command::{
|
||||
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
|
||||
tab_command, terminal_command, workflow_command,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::ResultExt;
|
||||
@@ -66,20 +63,18 @@ actions!(
|
||||
DeployHistory,
|
||||
DeployPromptLibrary,
|
||||
ConfirmCommand,
|
||||
NewContext,
|
||||
ToggleModelSelector,
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize)]
|
||||
pub enum InsertDraggedFiles {
|
||||
ProjectPaths(Vec<PathBuf>),
|
||||
ExternalFiles(Vec<PathBuf>),
|
||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct InlineAssist {
|
||||
prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl_actions!(assistant, [InsertDraggedFiles]);
|
||||
|
||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||
impl_actions!(assistant, [InlineAssist]);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct MessageId(clock::Lamport);
|
||||
@@ -189,7 +184,7 @@ impl Assistant {
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<Client>,
|
||||
stdout_is_a_pty: bool,
|
||||
dev_mode: bool,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<PromptBuilder> {
|
||||
cx.set_global(Assistant::default());
|
||||
@@ -221,26 +216,22 @@ pub fn init(
|
||||
})
|
||||
.detach();
|
||||
|
||||
context_store::init(&client.clone().into());
|
||||
context_store::init(&client);
|
||||
prompt_library::init(cx);
|
||||
init_language_model_settings(cx);
|
||||
assistant_slash_command::init(cx);
|
||||
assistant_tool::init(cx);
|
||||
assistant_panel::init(cx);
|
||||
context_servers::init(cx);
|
||||
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some(PromptOverrideContext {
|
||||
dev_mode,
|
||||
fs: fs.clone(),
|
||||
repo_path: stdout_is_a_pty
|
||||
.then(|| std::env::current_dir().log_err())
|
||||
.flatten(),
|
||||
cx,
|
||||
}))
|
||||
.log_err()
|
||||
.map(Arc::new)
|
||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||
register_slash_commands(Some(prompt_builder.clone()), cx);
|
||||
register_tools(cx);
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
@@ -376,7 +367,7 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
||||
|
||||
if let Some(prompt_builder) = prompt_builder {
|
||||
slash_command_registry.register_command(
|
||||
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
|
||||
workflow_command::WorkflowSlashCommand::new(prompt_builder),
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -414,11 +405,6 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
fn register_tools(cx: &mut AppContext) {
|
||||
let tool_registry = ToolRegistry::global(cx);
|
||||
tool_registry.register_tool(tools::now_tool::NowTool);
|
||||
}
|
||||
|
||||
pub fn humanize_token_count(count: usize) -> String {
|
||||
match count {
|
||||
0..=999 => count.to_string(),
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Pixels};
|
||||
use language_model::provider::open_ai;
|
||||
use language_model::settings::{
|
||||
AnthropicSettingsContent, AnthropicSettingsContentV1, OllamaSettingsContent,
|
||||
OpenAiSettingsContent, OpenAiSettingsContentV1, VersionedAnthropicSettingsContent,
|
||||
VersionedOpenAiSettingsContent,
|
||||
};
|
||||
use language_model::{settings::AllLanguageModelSettings, CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsSources};
|
||||
@@ -115,15 +109,16 @@ impl AssistantSettingsContent {
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.anthropic.is_none() {
|
||||
content.anthropic = Some(AnthropicSettingsContent::Versioned(
|
||||
VersionedAnthropicSettingsContent::V1(
|
||||
AnthropicSettingsContentV1 {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models: None,
|
||||
},
|
||||
),
|
||||
));
|
||||
content.anthropic =
|
||||
Some(language_model::settings::AnthropicSettingsContent::Versioned(
|
||||
language_model::settings::VersionedAnthropicSettingsContent::V1(
|
||||
language_model::settings::AnthropicSettingsContentV1 {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models: None
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -136,11 +131,11 @@ impl AssistantSettingsContent {
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.ollama.is_none() {
|
||||
content.ollama = Some(OllamaSettingsContent {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models: None,
|
||||
});
|
||||
content.ollama =
|
||||
Some(language_model::settings::OllamaSettingsContent {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -158,30 +153,23 @@ impl AssistantSettingsContent {
|
||||
models
|
||||
.into_iter()
|
||||
.filter_map(|model| match model {
|
||||
OpenAiModel::Custom {
|
||||
name,
|
||||
display_name,
|
||||
max_tokens,
|
||||
max_output_tokens,
|
||||
} => Some(open_ai::AvailableModel {
|
||||
name,
|
||||
display_name,
|
||||
max_tokens,
|
||||
max_output_tokens,
|
||||
}),
|
||||
open_ai::Model::Custom { name, max_tokens } => {
|
||||
Some(language_model::provider::open_ai::AvailableModel { name, max_tokens })
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
content.openai = Some(OpenAiSettingsContent::Versioned(
|
||||
VersionedOpenAiSettingsContent::V1(
|
||||
OpenAiSettingsContentV1 {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
),
|
||||
));
|
||||
content.openai =
|
||||
Some(language_model::settings::OpenAiSettingsContent::Versioned(
|
||||
language_model::settings::VersionedOpenAiSettingsContent::V1(
|
||||
language_model::settings::OpenAiSettingsContentV1 {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -307,7 +295,7 @@ impl AssistantSettingsContent {
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
default_model: Some(ollama::Model::new(&model)),
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
});
|
||||
@@ -328,7 +316,7 @@ impl AssistantSettingsContent {
|
||||
_ => (None, None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
default_model: open_ai::Model::from_id(&model).ok(),
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
@@ -341,7 +329,7 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
if let Ok(model) = open_ai::Model::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
@@ -555,8 +543,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-5-sonnet".into(),
|
||||
provider: "openai".into(),
|
||||
model: "gpt-4o".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use super::{MessageCacheMetadata, WorkflowStepEdit};
|
||||
use crate::{
|
||||
assistant_panel, prompt_library, slash_command::file_command, CacheStatus, Context,
|
||||
assistant_panel, prompt_library, slash_command::file_command, workflow::tool, Context,
|
||||
ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
||||
WorkflowStepEditKind,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
@@ -10,13 +8,15 @@ use assistant_slash_command::{
|
||||
SlashCommandRegistry,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use fs::FakeFs;
|
||||
use fs::{FakeFs, Fs as _};
|
||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use language_model::{LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use rand::prelude::*;
|
||||
use rope::Point;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
@@ -27,13 +27,10 @@ use std::{
|
||||
rc::Rc,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToPoint as _};
|
||||
use ui::{Context as _, WindowContext};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
RandomCharIter,
|
||||
};
|
||||
use util::{test::marked_text_ranges, RandomCharIter};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -476,16 +473,32 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
async fn test_edit_step_parsing(cx: &mut TestAppContext) {
|
||||
cx.update(prompt_library::init);
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(language::init);
|
||||
cx.update(Project::init_settings);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"hello.rs": r#"
|
||||
fn hello() {
|
||||
println!("Hello, World!");
|
||||
}
|
||||
"#.unindent()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
|
||||
let model = cx.read(|cx| {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.unwrap()
|
||||
});
|
||||
cx.update(assistant_panel::init);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
|
||||
@@ -500,382 +513,151 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
|
||||
|
||||
// Insert an assistant message to simulate a response.
|
||||
let assistant_message_id = context.update(cx, |context, cx| {
|
||||
let user_message_id = context.messages(cx).next().unwrap().id;
|
||||
context
|
||||
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
.id
|
||||
// Simulate user input
|
||||
let user_message = indoc! {r#"
|
||||
Please add unnecessary complexity to this code:
|
||||
|
||||
```hello.rs
|
||||
fn main() {
|
||||
println!("Hello, World!");
|
||||
}
|
||||
```
|
||||
"#};
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, user_message)], None, cx);
|
||||
});
|
||||
|
||||
// No edit tags
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
«one
|
||||
two
|
||||
»",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
",
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
|
||||
// Partial edit step tag is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
«
|
||||
<step»",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<step",
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
|
||||
// The rest of the step tag is added. The unclosed
|
||||
// step is treated as incomplete.
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<step«>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<edit>»",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<edit>»",
|
||||
&[&[]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// The full suggestion is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
// Simulate LLM response with edit steps
|
||||
let llm_response = indoc! {r#"
|
||||
Sure, I can help you with that. Here's a step-by-step process:
|
||||
|
||||
<step>
|
||||
Add a second function
|
||||
First, let's extract the greeting into a separate function:
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
fn greet() {
|
||||
println!("Hello, World!");
|
||||
}
|
||||
|
||||
<edit>«
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn one</search>
|
||||
<description>add a `two` function</description>
|
||||
</edit>
|
||||
fn main() {
|
||||
greet();
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
also,»",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn one</search>
|
||||
<description>add a `two` function</description>
|
||||
</edit>
|
||||
</step>»
|
||||
|
||||
also,",
|
||||
&[&[WorkflowStepEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: WorkflowStepEditKind::InsertAfter {
|
||||
search: "fn one".into(),
|
||||
description: "add a `two` function".into(),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// The step is manually edited.
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<step>
|
||||
Add a second function
|
||||
Now, let's make the greeting customizable:
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
fn greet(name: &str) {
|
||||
println!("Hello, {}!", name);
|
||||
}
|
||||
|
||||
<edit>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>«fn zero»</search>
|
||||
<description>add a `two` function</description>
|
||||
</edit>
|
||||
fn main() {
|
||||
greet("World");
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
also,",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
&context,
|
||||
"
|
||||
These changes make the code more modular and flexible.
|
||||
"#};
|
||||
|
||||
one
|
||||
two
|
||||
// Simulate the assist method to trigger the LLM response
|
||||
context.update(cx, |context, cx| context.assist(cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn zero</search>
|
||||
<description>add a `two` function</description>
|
||||
</edit>
|
||||
</step>»
|
||||
|
||||
also,",
|
||||
&[&[WorkflowStepEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: WorkflowStepEditKind::InsertAfter {
|
||||
search: "fn zero".into(),
|
||||
description: "add a `two` function".into(),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// When setting the message role to User, the steps are cleared.
|
||||
context.update(cx, |context, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
// Retrieve the assistant response message's start from the context
|
||||
let response_start_row = context.read_with(cx, |context, cx| {
|
||||
let buffer = context.buffer.read(cx);
|
||||
context.message_anchors[1].start.to_point(buffer).row
|
||||
});
|
||||
expect_steps(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
// Simulate the LLM completion
|
||||
model
|
||||
.as_fake()
|
||||
.stream_last_completion_response(llm_response.to_string());
|
||||
model.as_fake().end_last_completion_stream();
|
||||
|
||||
<step>
|
||||
Add a second function
|
||||
// Wait for the completion to be processed
|
||||
cx.run_until_parked();
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn zero</search>
|
||||
<description>add a `two` function</description>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
also,",
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
|
||||
// When setting the message role back to Assistant, the steps are reparsed.
|
||||
context.update(cx, |context, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
// Verify that the edit steps were parsed correctly
|
||||
context.read_with(cx, |context, cx| {
|
||||
assert_eq!(
|
||||
workflow_steps(context, cx),
|
||||
vec![
|
||||
(
|
||||
Point::new(response_start_row + 2, 0)..Point::new(response_start_row + 12, 3),
|
||||
WorkflowStepTestStatus::Pending
|
||||
),
|
||||
(
|
||||
Point::new(response_start_row + 14, 0)..Point::new(response_start_row + 24, 3),
|
||||
WorkflowStepTestStatus::Pending
|
||||
),
|
||||
]
|
||||
);
|
||||
});
|
||||
expect_steps(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn zero</search>
|
||||
<description>add a `two` function</description>
|
||||
</edit>
|
||||
</step>»
|
||||
|
||||
also,",
|
||||
&[&[WorkflowStepEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: WorkflowStepEditKind::InsertAfter {
|
||||
search: "fn zero".into(),
|
||||
description: "add a `two` function".into(),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// Ensure steps are re-parsed when deserializing.
|
||||
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
|
||||
let deserialized_context = cx.new_model(|cx| {
|
||||
Context::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
expect_steps(
|
||||
&deserialized_context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn zero</search>
|
||||
<description>add a `two` function</description>
|
||||
</edit>
|
||||
</step>»
|
||||
|
||||
also,",
|
||||
&[&[WorkflowStepEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: WorkflowStepEditKind::InsertAfter {
|
||||
search: "fn zero".into(),
|
||||
description: "add a `two` function".into(),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
|
||||
fn edit(context: &Model<Context>, new_text_marked_with_edits: &str, cx: &mut TestAppContext) {
|
||||
context.update(cx, |context, cx| {
|
||||
context.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
|
||||
});
|
||||
model
|
||||
.as_fake()
|
||||
.respond_to_last_tool_use(tool::WorkflowStepResolutionTool {
|
||||
step_title: "Title".into(),
|
||||
suggestions: vec![tool::WorkflowSuggestionTool {
|
||||
path: "/root/hello.rs".into(),
|
||||
// Simulate a symbol name that's slightly different than our outline query
|
||||
kind: tool::WorkflowSuggestionToolKind::Update {
|
||||
symbol: "fn main()".into(),
|
||||
description: "Extract a greeting function".into(),
|
||||
},
|
||||
}],
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Wait for tool use to be processed.
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify that the first edit step is not pending anymore.
|
||||
context.read_with(cx, |context, cx| {
|
||||
assert_eq!(
|
||||
workflow_steps(context, cx),
|
||||
vec![
|
||||
(
|
||||
Point::new(response_start_row + 2, 0)..Point::new(response_start_row + 12, 3),
|
||||
WorkflowStepTestStatus::Resolved
|
||||
),
|
||||
(
|
||||
Point::new(response_start_row + 14, 0)..Point::new(response_start_row + 24, 3),
|
||||
WorkflowStepTestStatus::Pending
|
||||
),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum WorkflowStepTestStatus {
|
||||
Pending,
|
||||
Resolved,
|
||||
Error,
|
||||
}
|
||||
|
||||
fn expect_steps(
|
||||
context: &Model<Context>,
|
||||
expected_marked_text: &str,
|
||||
expected_suggestions: &[&[WorkflowStepEdit]],
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
context.update(cx, |context, cx| {
|
||||
let expected_marked_text = expected_marked_text.unindent();
|
||||
let (expected_text, expected_ranges) = marked_text_ranges(&expected_marked_text, false);
|
||||
context.buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), expected_text);
|
||||
let ranges = context
|
||||
.workflow_steps
|
||||
.iter()
|
||||
.map(|entry| entry.range.to_offset(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
let marked = generate_marked_text(&expected_text, &ranges, false);
|
||||
assert_eq!(
|
||||
marked,
|
||||
expected_marked_text,
|
||||
"unexpected suggestion ranges. actual: {ranges:?}, expected: {expected_ranges:?}"
|
||||
);
|
||||
let suggestions = context
|
||||
.workflow_steps
|
||||
.iter()
|
||||
.map(|step| {
|
||||
step.edits
|
||||
.iter()
|
||||
.map(|edit| {
|
||||
let edit = edit.as_ref().unwrap();
|
||||
WorkflowStepEdit {
|
||||
path: edit.path.clone(),
|
||||
kind: edit.kind.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(suggestions, expected_suggestions);
|
||||
});
|
||||
});
|
||||
fn workflow_steps(
|
||||
context: &Context,
|
||||
cx: &AppContext,
|
||||
) -> Vec<(Range<Point>, WorkflowStepTestStatus)> {
|
||||
context
|
||||
.workflow_steps
|
||||
.iter()
|
||||
.map(|step| {
|
||||
let buffer = context.buffer.read(cx);
|
||||
let status = match &step.step.read(cx).resolution {
|
||||
None => WorkflowStepTestStatus::Pending,
|
||||
Some(Ok(_)) => WorkflowStepTestStatus::Resolved,
|
||||
Some(Err(_)) => WorkflowStepTestStatus::Error,
|
||||
};
|
||||
(step.range.to_point(buffer), status)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,7 +891,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
run_commands_in_text: false,
|
||||
})),
|
||||
true,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -1220,159 +1001,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context =
|
||||
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
// Create a test cache configuration
|
||||
let cache_configuration = &Some(LanguageModelCacheConfiguration {
|
||||
max_cache_anchors: 3,
|
||||
should_speculate: true,
|
||||
min_total_token: 10,
|
||||
});
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
.iter()
|
||||
.filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
|
||||
.count(),
|
||||
0,
|
||||
"Empty messages should not have any cache anchors."
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
|
||||
let message_2 = context
|
||||
.update(cx, |context, cx| {
|
||||
context.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
|
||||
let message_3 = context
|
||||
.update(cx, |context, cx| {
|
||||
context.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
.iter()
|
||||
.filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
|
||||
.count(),
|
||||
0,
|
||||
"Messages should not be marked for cache before going over the token minimum."
|
||||
);
|
||||
context.update(cx, |context, _| {
|
||||
context.token_count = Some(20);
|
||||
});
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, true, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
.iter()
|
||||
.map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
|
||||
.collect::<Vec<bool>>(),
|
||||
vec![true, true, false],
|
||||
"Last message should not be an anchor on speculative request."
|
||||
);
|
||||
|
||||
context
|
||||
.update(cx, |context, cx| {
|
||||
context.insert_message_after(message_3.id, Role::Assistant, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
.iter()
|
||||
.map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
|
||||
.collect::<Vec<bool>>(),
|
||||
vec![false, true, true, false],
|
||||
"Most recent message should also be cached if not a speculative request."
|
||||
);
|
||||
context.update(cx, |context, cx| {
|
||||
context.update_cache_status_for_completion(cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
.iter()
|
||||
.map(|(_, cache)| cache
|
||||
.as_ref()
|
||||
.map_or(None, |cache| Some(cache.status.clone())))
|
||||
.collect::<Vec<Option<CacheStatus>>>(),
|
||||
vec![
|
||||
Some(CacheStatus::Cached),
|
||||
Some(CacheStatus::Cached),
|
||||
Some(CacheStatus::Cached),
|
||||
None
|
||||
],
|
||||
"All user messages prior to anchor should be marked as cached."
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
.iter()
|
||||
.map(|(_, cache)| cache
|
||||
.as_ref()
|
||||
.map_or(None, |cache| Some(cache.status.clone())))
|
||||
.collect::<Vec<Option<CacheStatus>>>(),
|
||||
vec![
|
||||
Some(CacheStatus::Cached),
|
||||
Some(CacheStatus::Cached),
|
||||
Some(CacheStatus::Pending),
|
||||
None
|
||||
],
|
||||
"Modifying a message should invalidate it's cache but leave previous messages."
|
||||
);
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
.iter()
|
||||
.map(|(_, cache)| cache
|
||||
.as_ref()
|
||||
.map_or(None, |cache| Some(cache.status.clone())))
|
||||
.collect::<Vec<Option<CacheStatus>>>(),
|
||||
vec![
|
||||
Some(CacheStatus::Pending),
|
||||
Some(CacheStatus::Pending),
|
||||
Some(CacheStatus::Pending),
|
||||
None
|
||||
],
|
||||
"Modifying a message should invalidate all future messages."
|
||||
);
|
||||
}
|
||||
|
||||
fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role, Range<usize>)> {
|
||||
context
|
||||
.read(cx)
|
||||
@@ -1381,17 +1009,6 @@ fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role,
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn messages_cache(
|
||||
context: &Model<Context>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<(MessageId, Option<MessageCacheMetadata>)> {
|
||||
context
|
||||
.read(cx)
|
||||
.messages(cx)
|
||||
.map(|message| (message.id, message.cache.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FakeSlashCommand(String);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::{
|
||||
prompts::PromptBuilder, Context, ContextEvent, ContextId, ContextOperation, ContextVersion,
|
||||
SavedContext, SavedContextMetadata,
|
||||
};
|
||||
use ::proto::AnyProtoClient;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||
use clock::ReplicaId;
|
||||
@@ -26,7 +25,7 @@ use std::{
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub fn init(client: &AnyProtoClient) {
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
|
||||
client.add_model_request_handler(ContextStore::handle_open_context);
|
||||
client.add_model_request_handler(ContextStore::handle_create_context);
|
||||
@@ -162,7 +161,7 @@ impl ContextStore {
|
||||
) -> Result<proto::OpenContextResponse> {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
let operations = this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
if this.project.read(cx).is_remote() {
|
||||
return Err(anyhow!("only the host contexts can be opened"));
|
||||
}
|
||||
|
||||
@@ -191,7 +190,7 @@ impl ContextStore {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::CreateContextResponse> {
|
||||
let (context_id, operations) = this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
if this.project.read(cx).is_remote() {
|
||||
return Err(anyhow!("can only create contexts as the host"));
|
||||
}
|
||||
|
||||
@@ -235,7 +234,7 @@ impl ContextStore {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::SynchronizeContextsResponse> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
if this.project.read(cx).is_remote() {
|
||||
return Err(anyhow!("only the host can synchronize contexts"));
|
||||
}
|
||||
|
||||
@@ -357,7 +356,7 @@ impl ContextStore {
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow!("project was not remote")));
|
||||
};
|
||||
if project.is_local_or_ssh() {
|
||||
if project.is_local() {
|
||||
return Task::ready(Err(anyhow!("cannot create remote contexts as the host")));
|
||||
}
|
||||
|
||||
@@ -390,7 +389,7 @@ impl ContextStore {
|
||||
context_proto
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(ContextOperation::from_proto)
|
||||
.map(|op| ContextOperation::from_proto(op))
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.await?;
|
||||
@@ -488,7 +487,7 @@ impl ContextStore {
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow!("project was not remote")));
|
||||
};
|
||||
if project.is_local_or_ssh() {
|
||||
if project.is_local() {
|
||||
return Task::ready(Err(anyhow!("cannot open remote contexts as the host")));
|
||||
}
|
||||
|
||||
@@ -527,7 +526,7 @@ impl ContextStore {
|
||||
context_proto
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(ContextOperation::from_proto)
|
||||
.map(|op| ContextOperation::from_proto(op))
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.await?;
|
||||
@@ -590,7 +589,7 @@ impl ContextStore {
|
||||
};
|
||||
|
||||
// For now, only the host can advertise their open contexts.
|
||||
if self.project.read(cx).is_via_collab() {
|
||||
if self.project.read(cx).is_remote() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder,
|
||||
AssistantPanel, AssistantPanelEvent, CharOperation, LineDiff, LineOperation, ModelSelector,
|
||||
StreamingDiff,
|
||||
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
|
||||
CharOperation, LineDiff, LineOperation, ModelSelector, StreamingDiff,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{telemetry::Telemetry, ErrorExt};
|
||||
@@ -29,17 +28,16 @@ use gpui::{
|
||||
FontWeight, Global, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle,
|
||||
UpdateGlobal, View, ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
|
||||
use language::{Buffer, IndentKind, Point, TransactionId};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::Settings;
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
cmp,
|
||||
future::{self, Future},
|
||||
mem,
|
||||
ops::{Range, RangeInclusive},
|
||||
@@ -48,7 +46,7 @@ use std::{
|
||||
task::{self, Poll},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::OffsetRangeExt as _;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
|
||||
use util::{RangeExt, ResultExt};
|
||||
@@ -78,13 +76,8 @@ pub struct InlineAssistant {
|
||||
assists: HashMap<InlineAssistId, InlineAssist>,
|
||||
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
|
||||
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
|
||||
assist_observations: HashMap<
|
||||
InlineAssistId,
|
||||
(
|
||||
async_watch::Sender<AssistStatus>,
|
||||
async_watch::Receiver<AssistStatus>,
|
||||
),
|
||||
>,
|
||||
assist_observations:
|
||||
HashMap<InlineAssistId, (async_watch::Sender<()>, async_watch::Receiver<()>)>,
|
||||
confirmed_assists: HashMap<InlineAssistId, Model<Codegen>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
@@ -92,19 +85,6 @@ pub struct InlineAssistant {
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
pub enum AssistStatus {
|
||||
Idle,
|
||||
Started,
|
||||
Stopped,
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl AssistStatus {
|
||||
pub fn is_done(&self) -> bool {
|
||||
matches!(self, Self::Stopped | Self::Finished)
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
|
||||
impl InlineAssistant {
|
||||
@@ -133,21 +113,6 @@ impl InlineAssistant {
|
||||
Self::update_global(cx, |this, cx| this.handle_workspace_event(event, cx));
|
||||
})
|
||||
.detach();
|
||||
|
||||
let workspace = workspace.downgrade();
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let enabled = AssistantSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.asssistant_enabled(enabled, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_workspace_event(&mut self, event: &workspace::Event, cx: &mut WindowContext) {
|
||||
@@ -175,66 +140,81 @@ impl InlineAssistant {
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
struct CodegenRange {
|
||||
transform_range: Range<Point>,
|
||||
selection_ranges: Vec<Range<Point>>,
|
||||
focus_assist: bool,
|
||||
}
|
||||
|
||||
let mut selections = Vec::<Selection<Point>>::new();
|
||||
let mut newest_selection = None;
|
||||
for mut selection in editor.read(cx).selections.all::<Point>(cx) {
|
||||
if selection.end > selection.start {
|
||||
selection.start.column = 0;
|
||||
// If the selection ends at the start of the line, we don't want to include it.
|
||||
if selection.end.column == 0 {
|
||||
selection.end.row -= 1;
|
||||
}
|
||||
selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
|
||||
let newest_selection_range = editor.read(cx).selections.newest::<Point>(cx).range();
|
||||
let mut codegen_ranges: Vec<CodegenRange> = Vec::new();
|
||||
|
||||
let selection_ranges = snapshot
|
||||
.split_ranges(editor.read(cx).selections.disjoint_anchor_ranges())
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<Range<Point>>>();
|
||||
|
||||
for selection_range in selection_ranges {
|
||||
let selection_is_newest = newest_selection_range.contains_inclusive(&selection_range);
|
||||
let mut transform_range = selection_range.start..selection_range.end;
|
||||
|
||||
// Expand the transform range to start/end of lines.
|
||||
// If a non-empty selection ends at the start of the last line, clip at the end of the penultimate line.
|
||||
transform_range.start.column = 0;
|
||||
if transform_range.end.column == 0 && transform_range.end > transform_range.start {
|
||||
transform_range.end.row -= 1;
|
||||
}
|
||||
transform_range.end.column = snapshot.line_len(MultiBufferRow(transform_range.end.row));
|
||||
let selection_range =
|
||||
selection_range.start..selection_range.end.min(transform_range.end);
|
||||
|
||||
if let Some(prev_selection) = selections.last_mut() {
|
||||
if selection.start <= prev_selection.end {
|
||||
prev_selection.end = selection.end;
|
||||
// If we intersect the previous transform range,
|
||||
if let Some(CodegenRange {
|
||||
transform_range: prev_transform_range,
|
||||
selection_ranges,
|
||||
focus_assist,
|
||||
}) = codegen_ranges.last_mut()
|
||||
{
|
||||
if transform_range.start <= prev_transform_range.end {
|
||||
prev_transform_range.end = transform_range.end;
|
||||
selection_ranges.push(selection_range);
|
||||
*focus_assist |= selection_is_newest;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let latest_selection = newest_selection.get_or_insert_with(|| selection.clone());
|
||||
if selection.id > latest_selection.id {
|
||||
*latest_selection = selection.clone();
|
||||
}
|
||||
selections.push(selection);
|
||||
}
|
||||
let newest_selection = newest_selection.unwrap();
|
||||
|
||||
let mut codegen_ranges = Vec::new();
|
||||
for (excerpt_id, buffer, buffer_range) in
|
||||
snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
|
||||
snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
|
||||
}))
|
||||
{
|
||||
let start = Anchor {
|
||||
buffer_id: Some(buffer.remote_id()),
|
||||
excerpt_id,
|
||||
text_anchor: buffer.anchor_before(buffer_range.start),
|
||||
};
|
||||
let end = Anchor {
|
||||
buffer_id: Some(buffer.remote_id()),
|
||||
excerpt_id,
|
||||
text_anchor: buffer.anchor_after(buffer_range.end),
|
||||
};
|
||||
codegen_ranges.push(start..end);
|
||||
codegen_ranges.push(CodegenRange {
|
||||
transform_range,
|
||||
selection_ranges: vec![selection_range],
|
||||
focus_assist: selection_is_newest,
|
||||
})
|
||||
}
|
||||
|
||||
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||
let prompt_buffer =
|
||||
cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx));
|
||||
let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
|
||||
|
||||
let mut assists = Vec::new();
|
||||
let mut assist_to_focus = None;
|
||||
for range in codegen_ranges {
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
|
||||
for CodegenRange {
|
||||
transform_range,
|
||||
selection_ranges,
|
||||
focus_assist,
|
||||
} in codegen_ranges
|
||||
{
|
||||
let transform_range = snapshot.anchor_before(transform_range.start)
|
||||
..snapshot.anchor_after(transform_range.end);
|
||||
let selection_ranges = selection_ranges
|
||||
.iter()
|
||||
.map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
editor.read(cx).buffer().clone(),
|
||||
range.clone(),
|
||||
transform_range.clone(),
|
||||
selection_ranges,
|
||||
None,
|
||||
self.telemetry.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
@@ -242,6 +222,7 @@ impl InlineAssistant {
|
||||
)
|
||||
});
|
||||
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
PromptEditor::new(
|
||||
@@ -258,23 +239,16 @@ impl InlineAssistant {
|
||||
)
|
||||
});
|
||||
|
||||
if assist_to_focus.is_none() {
|
||||
let focus_assist = if newest_selection.reversed {
|
||||
range.start.to_point(&snapshot) == newest_selection.start
|
||||
} else {
|
||||
range.end.to_point(&snapshot) == newest_selection.end
|
||||
};
|
||||
if focus_assist {
|
||||
assist_to_focus = Some(assist_id);
|
||||
}
|
||||
if focus_assist {
|
||||
assist_to_focus = Some(assist_id);
|
||||
}
|
||||
|
||||
let [prompt_block_id, end_block_id] =
|
||||
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
|
||||
self.insert_assist_blocks(editor, &transform_range, &prompt_editor, cx);
|
||||
|
||||
assists.push((
|
||||
assist_id,
|
||||
range,
|
||||
transform_range,
|
||||
prompt_editor,
|
||||
prompt_block_id,
|
||||
end_block_id,
|
||||
@@ -341,6 +315,7 @@ impl InlineAssistant {
|
||||
Codegen::new(
|
||||
editor.read(cx).buffer().clone(),
|
||||
range.clone(),
|
||||
vec![range.clone()],
|
||||
initial_transaction_id,
|
||||
self.telemetry.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
@@ -950,17 +925,12 @@ impl InlineAssistant {
|
||||
assist
|
||||
.codegen
|
||||
.update(cx, |codegen, cx| {
|
||||
codegen.start(
|
||||
assist.range.clone(),
|
||||
user_prompt,
|
||||
assistant_panel_context,
|
||||
cx,
|
||||
)
|
||||
codegen.start(user_prompt, assistant_panel_context, cx)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
|
||||
tx.send(AssistStatus::Started).ok();
|
||||
tx.send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +944,7 @@ impl InlineAssistant {
|
||||
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
|
||||
|
||||
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
|
||||
tx.send(AssistStatus::Stopped).ok();
|
||||
tx.send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,7 +1109,7 @@ impl InlineAssistant {
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(false);
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..=Anchor::max(),
|
||||
Some(cx.theme().status().deleted_background),
|
||||
@@ -1176,14 +1146,11 @@ impl InlineAssistant {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn observe_assist(
|
||||
&mut self,
|
||||
assist_id: InlineAssistId,
|
||||
) -> async_watch::Receiver<AssistStatus> {
|
||||
pub fn observe_assist(&mut self, assist_id: InlineAssistId) -> async_watch::Receiver<()> {
|
||||
if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
|
||||
rx.clone()
|
||||
} else {
|
||||
let (tx, rx) = async_watch::channel(AssistStatus::Idle);
|
||||
let (tx, rx) = async_watch::channel(());
|
||||
self.assist_observations.insert(assist_id, (tx, rx.clone()));
|
||||
rx
|
||||
}
|
||||
@@ -1203,11 +1170,9 @@ impl InlineAssistStatus {
|
||||
pub(crate) fn is_pending(&self) -> bool {
|
||||
matches!(self, Self::Pending)
|
||||
}
|
||||
|
||||
pub(crate) fn is_confirmed(&self) -> bool {
|
||||
matches!(self, Self::Confirmed)
|
||||
}
|
||||
|
||||
pub(crate) fn is_done(&self) -> bool {
|
||||
matches!(self, Self::Done)
|
||||
}
|
||||
@@ -1797,16 +1762,13 @@ impl PromptEditor {
|
||||
CodegenStatus::Pending => {
|
||||
cx.emit(PromptEditorEvent::DismissRequested);
|
||||
}
|
||||
CodegenStatus::Done => {
|
||||
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
if self.edited_since_done {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
} else {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested);
|
||||
}
|
||||
}
|
||||
CodegenStatus::Error(_) => {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1921,7 +1883,7 @@ impl PromptEditor {
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: settings.ui_font_size.into(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
@@ -2122,7 +2084,7 @@ impl InlineAssist {
|
||||
if assist.decorations.is_none() {
|
||||
this.finish_assist(assist_id, false, cx);
|
||||
} else if let Some(tx) = this.assist_observations.get(&assist_id) {
|
||||
tx.0.send(AssistStatus::Finished).ok();
|
||||
tx.0.send(()).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2158,12 +2120,9 @@ impl InlineAssist {
|
||||
return future::ready(Err(anyhow!("no user prompt"))).boxed();
|
||||
};
|
||||
let assistant_panel_context = self.assistant_panel_context(cx);
|
||||
self.codegen.read(cx).count_tokens(
|
||||
self.range.clone(),
|
||||
user_prompt,
|
||||
assistant_panel_context,
|
||||
cx,
|
||||
)
|
||||
self.codegen
|
||||
.read(cx)
|
||||
.count_tokens(user_prompt, assistant_panel_context, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2184,6 +2143,8 @@ pub struct Codegen {
|
||||
buffer: Model<MultiBuffer>,
|
||||
old_buffer: Model<Buffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
transform_range: Range<Anchor>,
|
||||
selected_ranges: Vec<Range<Anchor>>,
|
||||
edit_position: Option<Anchor>,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
@@ -2193,7 +2154,7 @@ pub struct Codegen {
|
||||
diff: Diff,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
_subscription: gpui::Subscription,
|
||||
builder: Arc<PromptBuilder>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
enum CodegenStatus {
|
||||
@@ -2220,7 +2181,8 @@ impl EventEmitter<CodegenEvent> for Codegen {}
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
transform_range: Range<Anchor>,
|
||||
selected_ranges: Vec<Range<Anchor>>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
@@ -2230,7 +2192,7 @@ impl Codegen {
|
||||
|
||||
let (old_buffer, _, _) = buffer
|
||||
.read(cx)
|
||||
.range_to_buffer_ranges(range.clone(), cx)
|
||||
.range_to_buffer_ranges(transform_range.clone(), cx)
|
||||
.pop()
|
||||
.unwrap();
|
||||
let old_buffer = cx.new_model(|cx| {
|
||||
@@ -2261,7 +2223,9 @@ impl Codegen {
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
initial_transaction_id,
|
||||
builder,
|
||||
prompt_builder: builder,
|
||||
transform_range,
|
||||
selected_ranges,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2286,14 +2250,12 @@ impl Codegen {
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
edit_range: Range<Anchor>,
|
||||
user_prompt: String,
|
||||
assistant_panel_context: Option<LanguageModelRequest>,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<TokenCounts>> {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
let request =
|
||||
self.build_request(user_prompt, assistant_panel_context.clone(), edit_range, cx);
|
||||
let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx);
|
||||
match request {
|
||||
Ok(request) => {
|
||||
let total_count = model.count_tokens(request.clone(), cx);
|
||||
@@ -2318,7 +2280,6 @@ impl Codegen {
|
||||
|
||||
pub fn start(
|
||||
&mut self,
|
||||
edit_range: Range<Anchor>,
|
||||
user_prompt: String,
|
||||
assistant_panel_context: Option<LanguageModelRequest>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -2333,24 +2294,20 @@ impl Codegen {
|
||||
});
|
||||
}
|
||||
|
||||
self.edit_position = Some(edit_range.start.bias_right(&self.snapshot));
|
||||
self.edit_position = Some(self.transform_range.start.bias_right(&self.snapshot));
|
||||
|
||||
let telemetry_id = model.telemetry_id();
|
||||
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> = if user_prompt
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "delete"
|
||||
{
|
||||
async { Ok(stream::empty().boxed()) }.boxed_local()
|
||||
} else {
|
||||
let request =
|
||||
self.build_request(user_prompt, assistant_panel_context, edit_range.clone(), cx)?;
|
||||
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(stream::empty().boxed()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
|
||||
|
||||
let chunks =
|
||||
cx.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await });
|
||||
async move { Ok(chunks.await?.boxed()) }.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, edit_range, chunks, cx);
|
||||
let chunks =
|
||||
cx.spawn(|_, cx| async move { model.stream_completion(request, &cx).await });
|
||||
async move { Ok(chunks.await?.boxed()) }.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, self.transform_range.clone(), chunks, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2358,11 +2315,10 @@ impl Codegen {
|
||||
&self,
|
||||
user_prompt: String,
|
||||
assistant_panel_context: Option<LanguageModelRequest>,
|
||||
edit_range: Range<Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Result<LanguageModelRequest> {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let language = buffer.language_at(edit_range.start);
|
||||
let language = buffer.language_at(self.transform_range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
|
||||
None
|
||||
@@ -2373,10 +2329,23 @@ impl Codegen {
|
||||
None
|
||||
};
|
||||
|
||||
let language_name = language_name.as_ref();
|
||||
let start = buffer.point_to_buffer_offset(edit_range.start);
|
||||
let end = buffer.point_to_buffer_offset(edit_range.end);
|
||||
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
|
||||
// Higher Temperature increases the randomness of model outputs.
|
||||
// If Markdown or No Language is Known, increase the randomness for more creative output
|
||||
// If Code, decrease temperature to get more deterministic outputs
|
||||
let temperature = if let Some(language) = language_name.clone() {
|
||||
if language.as_ref() == "Markdown" {
|
||||
1.0
|
||||
} else {
|
||||
0.5
|
||||
}
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let language_name = language_name.as_deref();
|
||||
let start = buffer.point_to_buffer_offset(self.transform_range.start);
|
||||
let end = buffer.point_to_buffer_offset(self.transform_range.end);
|
||||
let (transform_buffer, transform_range) = if let Some((start, end)) = start.zip(end) {
|
||||
let (start_buffer, start_buffer_offset) = start;
|
||||
let (end_buffer, end_buffer_offset) = end;
|
||||
if start_buffer.remote_id() == end_buffer.remote_id() {
|
||||
@@ -2388,9 +2357,39 @@ impl Codegen {
|
||||
return Err(anyhow::anyhow!("invalid transformation range"));
|
||||
};
|
||||
|
||||
let mut transform_context_range = transform_range.to_point(&transform_buffer);
|
||||
transform_context_range.start.row = transform_context_range.start.row.saturating_sub(3);
|
||||
transform_context_range.start.column = 0;
|
||||
transform_context_range.end =
|
||||
(transform_context_range.end + Point::new(3, 0)).min(transform_buffer.max_point());
|
||||
transform_context_range.end.column =
|
||||
transform_buffer.line_len(transform_context_range.end.row);
|
||||
let transform_context_range = transform_context_range.to_offset(&transform_buffer);
|
||||
|
||||
let selected_ranges = self
|
||||
.selected_ranges
|
||||
.iter()
|
||||
.filter_map(|selected_range| {
|
||||
let start = buffer
|
||||
.point_to_buffer_offset(selected_range.start)
|
||||
.map(|(_, offset)| offset)?;
|
||||
let end = buffer
|
||||
.point_to_buffer_offset(selected_range.end)
|
||||
.map(|(_, offset)| offset)?;
|
||||
Some(start..end)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let prompt = self
|
||||
.builder
|
||||
.generate_content_prompt(user_prompt, language_name, buffer, range)
|
||||
.prompt_builder
|
||||
.generate_content_prompt(
|
||||
user_prompt,
|
||||
language_name,
|
||||
transform_buffer,
|
||||
transform_range,
|
||||
selected_ranges,
|
||||
transform_context_range,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
|
||||
|
||||
let mut messages = Vec::new();
|
||||
@@ -2406,9 +2405,8 @@ impl Codegen {
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: vec!["|END|>".to_string()],
|
||||
temperature: 1.,
|
||||
temperature,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2464,84 +2462,19 @@ impl Codegen {
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
|
||||
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() {
|
||||
let char_ops = diff.push_new(&new_text);
|
||||
line_diff
|
||||
.push_char_operations(&char_ops, &selected_text);
|
||||
diff_tx
|
||||
.send((char_ops, line_diff.line_operations()))
|
||||
.await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
let char_ops = diff.push_new("\n");
|
||||
line_diff
|
||||
.push_char_operations(&char_ops, &selected_text);
|
||||
diff_tx
|
||||
.send((char_ops, line_diff.line_operations()))
|
||||
.await?;
|
||||
if line_indent.is_none() {
|
||||
// Don't write out the leading indentation in empty lines on the next line
|
||||
// This is the case where the above if statement didn't clear the buffer
|
||||
new_text.clear();
|
||||
}
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
let char_ops = diff.push_new(&chunk);
|
||||
line_diff.push_char_operations(&char_ops, &selected_text);
|
||||
diff_tx
|
||||
.send((char_ops, line_diff.line_operations()))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut char_ops = diff.push_new(&new_text);
|
||||
char_ops.extend(diff.finish());
|
||||
let char_ops = diff.finish();
|
||||
line_diff.push_char_operations(&char_ops, &selected_text);
|
||||
line_diff.finish(&selected_text);
|
||||
diff_tx
|
||||
@@ -3005,311 +2938,13 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
||||
mod tests {
|
||||
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 language_model::LanguageModelRegistry;
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
use std::{future, sync::Arc};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_model::LanguageModelRegistry::test);
|
||||
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 prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
range,
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
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);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
drop(chunks_tx);
|
||||
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,
|
||||
) {
|
||||
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 range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
range.clone(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
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);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
drop(chunks_tx);
|
||||
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,
|
||||
) {
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
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 range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
range.clone(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
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);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
drop(chunks_tx);
|
||||
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_respects_tabs_in_selection(cx: &mut TestAppContext) {
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
func main() {
|
||||
\tx := 0
|
||||
\tfor i := 0; i < 10; i++ {
|
||||
\t\tx++
|
||||
\t}
|
||||
}
|
||||
"};
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text, 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(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
range.clone(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let new_text = concat!(
|
||||
"func main() {\n",
|
||||
"\tx := 0\n",
|
||||
"\tfor x < 10 {\n",
|
||||
"\t\tx++\n",
|
||||
"\t}", //
|
||||
);
|
||||
chunks_tx.unbounded_send(new_text.to_string()).unwrap();
|
||||
drop(chunks_tx);
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
func main() {
|
||||
\tx := 0
|
||||
\tfor x < 10 {
|
||||
\t\tx++
|
||||
\t}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
|
||||
@@ -3349,27 +2984,4 @@ mod tests {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use feature_flags::ZedPro;
|
||||
use gpui::Action;
|
||||
use gpui::DismissEvent;
|
||||
|
||||
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
|
||||
use proto::Plan;
|
||||
use workspace::ShowConfiguration;
|
||||
@@ -37,7 +36,7 @@ pub struct ModelPickerDelegate {
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconName,
|
||||
provider_icon: IconName,
|
||||
availability: LanguageModelAvailability,
|
||||
is_selected: bool,
|
||||
}
|
||||
@@ -150,8 +149,6 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
let model_info = self.filtered_models.get(ix)?;
|
||||
let show_badges = cx.has_flag::<ZedPro>();
|
||||
let provider_name: String = model_info.model.provider_name().0.into();
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
@@ -159,7 +156,7 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
.selected(selected)
|
||||
.start_slot(
|
||||
div().pr_1().child(
|
||||
Icon::new(model_info.icon)
|
||||
Icon::new(model_info.provider_icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Medium),
|
||||
),
|
||||
@@ -169,16 +166,11 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.font_buffer(cx)
|
||||
.min_w(px(240.))
|
||||
.min_w(px(200.))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(model_info.model.name().0.clone()))
|
||||
.child(
|
||||
Label::new(provider_name)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(match model_info.availability {
|
||||
LanguageModelAvailability::Public => None,
|
||||
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
|
||||
@@ -190,15 +182,15 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
})
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.end_slot(div().when(model_info.is_selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.child(div().when(model_info.is_selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -269,17 +261,16 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
let provider_id = provider.id();
|
||||
let icon = provider.icon();
|
||||
let provider_icon = provider.icon();
|
||||
let selected_model = selected_model.clone();
|
||||
let selected_provider = selected_provider.clone();
|
||||
|
||||
provider.provided_models(cx).into_iter().map(move |model| {
|
||||
let model = model.clone();
|
||||
let icon = model.icon().unwrap_or(icon);
|
||||
|
||||
ModelInfo {
|
||||
model: model.clone(),
|
||||
icon,
|
||||
provider_icon,
|
||||
availability: model.availability(),
|
||||
is_selected: selected_model.as_ref() == Some(&model.id())
|
||||
&& selected_provider.as_ref() == Some(&provider_id),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssistant};
|
||||
use crate::{
|
||||
slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssist, InlineAssistant,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -23,7 +25,6 @@ use language_model::{
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use release_channel::ReleaseChannel;
|
||||
use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -43,7 +44,6 @@ use ui::{
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use workspace::Workspace;
|
||||
use zed_actions::InlineAssist;
|
||||
|
||||
actions!(
|
||||
prompt_library,
|
||||
@@ -95,16 +95,14 @@ pub fn open_prompt_library(
|
||||
cx.spawn(|cx| async move {
|
||||
let store = store.await?;
|
||||
cx.update(|cx| {
|
||||
let app_id = ReleaseChannel::global(cx).app_id();
|
||||
let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some("Prompt Library".into()),
|
||||
appears_transparent: cfg!(target_os = "macos"),
|
||||
appears_transparent: !cfg!(windows),
|
||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||
}),
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -498,7 +496,7 @@ impl PromptLibrary {
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
|
||||
if prompt_id.is_built_in() {
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(false);
|
||||
}
|
||||
editor
|
||||
});
|
||||
@@ -513,7 +511,7 @@ impl PromptLibrary {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
if prompt_id.is_built_in() {
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(false);
|
||||
}
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
@@ -794,7 +792,6 @@ impl PromptLibrary {
|
||||
content: vec![body.to_string().into()],
|
||||
cache: false,
|
||||
}],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: 1.,
|
||||
},
|
||||
@@ -929,7 +926,6 @@ impl PromptLibrary {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
..EditorStyle::default()
|
||||
},
|
||||
)),
|
||||
),
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::AssetSource;
|
||||
use handlebars::{Handlebars, RenderError};
|
||||
use language::{BufferSnapshot, LanguageName};
|
||||
use handlebars::{Handlebars, RenderError, TemplateError};
|
||||
use language::BufferSnapshot;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use text::LineEnding;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptContext {
|
||||
pub content_type: String,
|
||||
pub language_name: Option<String>,
|
||||
pub is_insert: bool,
|
||||
pub is_truncated: bool,
|
||||
pub document_content: String,
|
||||
pub user_prompt: String,
|
||||
pub rewrite_section: Option<String>,
|
||||
pub rewrite_section: String,
|
||||
pub rewrite_section_prefix: String,
|
||||
pub rewrite_section_suffix: String,
|
||||
pub rewrite_section_with_edits: String,
|
||||
pub has_insertion: bool,
|
||||
pub has_replacement: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -41,162 +42,128 @@ pub struct StepResolutionContext {
|
||||
pub step_to_resolve: String,
|
||||
}
|
||||
|
||||
pub struct PromptLoadingParams<'a> {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub repo_path: Option<PathBuf>,
|
||||
pub cx: &'a gpui::AppContext,
|
||||
}
|
||||
|
||||
pub struct PromptBuilder {
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
}
|
||||
|
||||
pub struct PromptOverrideContext<'a> {
|
||||
pub dev_mode: bool,
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub cx: &'a mut gpui::AppContext,
|
||||
}
|
||||
|
||||
impl PromptBuilder {
|
||||
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
|
||||
pub fn new(override_cx: Option<PromptOverrideContext>) -> Result<Self, Box<TemplateError>> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
Self::register_built_in_templates(&mut handlebars)?;
|
||||
Self::register_templates(&mut handlebars)?;
|
||||
|
||||
let handlebars = Arc::new(Mutex::new(handlebars));
|
||||
|
||||
if let Some(params) = loading_params {
|
||||
Self::watch_fs_for_template_overrides(params, handlebars.clone());
|
||||
if let Some(override_cx) = override_cx {
|
||||
Self::watch_fs_for_template_overrides(override_cx, handlebars.clone());
|
||||
}
|
||||
|
||||
Ok(Self { handlebars })
|
||||
}
|
||||
|
||||
/// Watches the filesystem for changes to prompt template overrides.
|
||||
///
|
||||
/// This function sets up a file watcher on the prompt templates directory. It performs
|
||||
/// an initial scan of the directory and registers any existing template overrides.
|
||||
/// Then it continuously monitors for changes, reloading templates as they are
|
||||
/// modified or added.
|
||||
///
|
||||
/// If the templates directory doesn't exist initially, it waits for it to be created.
|
||||
/// If the directory is removed, it restores the built-in templates and waits for the
|
||||
/// directory to be recreated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
|
||||
/// and application context.
|
||||
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
|
||||
fn watch_fs_for_template_overrides(
|
||||
mut params: PromptLoadingParams,
|
||||
PromptOverrideContext { dev_mode, fs, cx }: PromptOverrideContext,
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
) {
|
||||
params.repo_path = None;
|
||||
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
|
||||
params.cx.background_executor()
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(parent_dir) = templates_dir.parent() else {
|
||||
return;
|
||||
let templates_dir = if dev_mode {
|
||||
std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|pwd| {
|
||||
let pwd_assets_prompts = pwd.join("assets").join("prompts");
|
||||
pwd_assets_prompts.exists().then_some(pwd_assets_prompts)
|
||||
})
|
||||
.unwrap_or_else(|| paths::prompt_overrides_dir().clone())
|
||||
} else {
|
||||
paths::prompt_overrides_dir().clone()
|
||||
};
|
||||
|
||||
let mut found_dir_once = false;
|
||||
loop {
|
||||
// Check if the templates directory exists and handle its status
|
||||
// If it exists, log its presence and check if it's a symlink
|
||||
// If it doesn't exist:
|
||||
// - Log that we're using built-in prompts
|
||||
// - Check if it's a broken symlink and log if so
|
||||
// - Set up a watcher to detect when it's created
|
||||
// After the first check, set the `found_dir_once` flag
|
||||
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
|
||||
let dir_status = params.fs.is_dir(&templates_dir).await;
|
||||
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
|
||||
if dir_status {
|
||||
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
} else {
|
||||
if !found_dir_once {
|
||||
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
|
||||
}
|
||||
}
|
||||
|
||||
if params.fs.is_dir(parent_dir).await {
|
||||
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
while let Some(changed_paths) = changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
|
||||
if let Ok(target) = params.fs.read_link(&templates_dir).await {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// Create the prompt templates directory if it doesn't exist
|
||||
if !fs.is_dir(&templates_dir).await {
|
||||
if let Err(e) = fs.create_dir(&templates_dir).await {
|
||||
log::error!("Failed to create prompt templates directory: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
found_dir_once = true;
|
||||
|
||||
// Initial scan of the prompt overrides directory
|
||||
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
|
||||
while let Some(Ok(file_path)) = entries.next().await {
|
||||
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||
if let Ok(content) = params.fs.load(&file_path).await {
|
||||
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||
log::info!("Registering prompt template override: {}", file_name);
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch both the parent directory and the template overrides directory:
|
||||
// - Monitor the parent directory to detect if the template overrides directory is deleted.
|
||||
// - Monitor the template overrides directory to re-register templates when they change.
|
||||
// Combine both watch streams into a single stream.
|
||||
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
|
||||
let mut combined_changes = futures::stream::select(changes, parent_changes);
|
||||
|
||||
while let Some(changed_paths) = combined_changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
if !params.fs.is_dir(&templates_dir).await {
|
||||
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
|
||||
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
|
||||
break;
|
||||
}
|
||||
}
|
||||
for event in changed_paths {
|
||||
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
|
||||
log::info!("Reloading prompt template override: {}", event.path.display());
|
||||
if let Some(content) = params.fs.load(&event.path).await.log_err() {
|
||||
let file_name = event.path.file_stem().unwrap().to_string_lossy();
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(watcher);
|
||||
drop(parent_watcher);
|
||||
}
|
||||
|
||||
// Initial scan of the prompts directory
|
||||
if let Ok(mut entries) = fs.read_dir(&templates_dir).await {
|
||||
while let Some(Ok(file_path)) = entries.next().await {
|
||||
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||
if let Ok(content) = fs.load(&file_path).await {
|
||||
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||
|
||||
match handlebars.lock().register_template_string(&file_name, content) {
|
||||
Ok(_) => {
|
||||
log::info!(
|
||||
"Successfully registered template override: {} ({})",
|
||||
file_name,
|
||||
file_path.display()
|
||||
);
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to register template during initial scan: {} ({})",
|
||||
e,
|
||||
file_path.display()
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes
|
||||
let (mut changes, watcher) = fs.watch(&templates_dir, Duration::from_secs(1)).await;
|
||||
while let Some(changed_paths) = changes.next().await {
|
||||
for changed_path in changed_paths {
|
||||
if changed_path.extension().map_or(false, |ext| ext == "hbs") {
|
||||
log::info!("Reloading template: {}", changed_path.display());
|
||||
if let Some(content) = fs.load(&changed_path).await.log_err() {
|
||||
let file_name = changed_path.file_stem().unwrap().to_string_lossy();
|
||||
let file_path = changed_path.to_string_lossy();
|
||||
match handlebars.lock().register_template_string(&file_name, content) {
|
||||
Ok(_) => log::info!(
|
||||
"Successfully reloaded template: {} ({})",
|
||||
file_name,
|
||||
file_path
|
||||
),
|
||||
Err(e) => log::error!(
|
||||
"Failed to register template: {} ({})",
|
||||
e,
|
||||
file_path
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(watcher);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
|
||||
for path in Assets.list("prompts")? {
|
||||
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
|
||||
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
|
||||
log::info!("Registering built-in prompt template: {}", id);
|
||||
let prompt = String::from_utf8_lossy(prompt.as_ref());
|
||||
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
|
||||
}
|
||||
}
|
||||
}
|
||||
fn register_templates(handlebars: &mut Handlebars) -> Result<(), Box<TemplateError>> {
|
||||
let mut register_template = |id: &str| {
|
||||
let prompt = Assets::get(&format!("prompts/{}.hbs", id))
|
||||
.unwrap_or_else(|| panic!("{} prompt template not found", id))
|
||||
.data;
|
||||
handlebars
|
||||
.register_template_string(id, String::from_utf8_lossy(&prompt))
|
||||
.map_err(Box::new)
|
||||
};
|
||||
|
||||
register_template("content_prompt")?;
|
||||
register_template("terminal_assistant_prompt")?;
|
||||
register_template("edit_workflow")?;
|
||||
register_template("step_resolution")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -204,31 +171,32 @@ impl PromptBuilder {
|
||||
pub fn generate_content_prompt(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
language_name: Option<&LanguageName>,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
transform_range: Range<usize>,
|
||||
selected_ranges: Vec<Range<usize>>,
|
||||
transform_context_range: Range<usize>,
|
||||
) -> Result<String, RenderError> {
|
||||
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => "text",
|
||||
Some(_) => "code",
|
||||
};
|
||||
|
||||
const MAX_CTX: usize = 50000;
|
||||
let is_insert = range.is_empty();
|
||||
let mut is_truncated = false;
|
||||
|
||||
let before_range = 0..range.start;
|
||||
let before_range = 0..transform_range.start;
|
||||
let truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.start - MAX_CTX..range.start
|
||||
transform_range.start - MAX_CTX..transform_range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
|
||||
let after_range = range.end..buffer.len();
|
||||
let after_range = transform_range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.end..range.end + MAX_CTX
|
||||
transform_range.end..transform_range.end + MAX_CTX
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
@@ -237,37 +205,74 @@ impl PromptBuilder {
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
if is_insert {
|
||||
document_content.push_str("<insert_here></insert_here>");
|
||||
} else {
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(transform_range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
|
||||
let rewrite_section = if !is_insert {
|
||||
let mut section = String::new();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
section.push_str(chunk);
|
||||
let mut rewrite_section = String::new();
|
||||
for chunk in buffer.text_for_range(transform_range.clone()) {
|
||||
rewrite_section.push_str(chunk);
|
||||
}
|
||||
|
||||
let mut rewrite_section_prefix = String::new();
|
||||
for chunk in buffer.text_for_range(transform_context_range.start..transform_range.start) {
|
||||
rewrite_section_prefix.push_str(chunk);
|
||||
}
|
||||
|
||||
let mut rewrite_section_suffix = String::new();
|
||||
for chunk in buffer.text_for_range(transform_range.end..transform_context_range.end) {
|
||||
rewrite_section_suffix.push_str(chunk);
|
||||
}
|
||||
|
||||
let rewrite_section_with_edits = {
|
||||
let mut section_with_selections = String::new();
|
||||
let mut last_end = 0;
|
||||
for selected_range in &selected_ranges {
|
||||
if selected_range.start > last_end {
|
||||
section_with_selections.push_str(
|
||||
&rewrite_section[last_end..selected_range.start - transform_range.start],
|
||||
);
|
||||
}
|
||||
if selected_range.start == selected_range.end {
|
||||
section_with_selections.push_str("<insert_here></insert_here>");
|
||||
} else {
|
||||
section_with_selections.push_str("<edit_here>");
|
||||
section_with_selections.push_str(
|
||||
&rewrite_section[selected_range.start - transform_range.start
|
||||
..selected_range.end - transform_range.start],
|
||||
);
|
||||
section_with_selections.push_str("</edit_here>");
|
||||
}
|
||||
last_end = selected_range.end - transform_range.start;
|
||||
}
|
||||
Some(section)
|
||||
} else {
|
||||
None
|
||||
if last_end < rewrite_section.len() {
|
||||
section_with_selections.push_str(&rewrite_section[last_end..]);
|
||||
}
|
||||
section_with_selections
|
||||
};
|
||||
|
||||
let has_insertion = selected_ranges.iter().any(|range| range.start == range.end);
|
||||
let has_replacement = selected_ranges.iter().any(|range| range.start != range.end);
|
||||
|
||||
let context = ContentPromptContext {
|
||||
content_type: content_type.to_string(),
|
||||
language_name: language_name.map(|s| s.to_string()),
|
||||
is_insert,
|
||||
is_truncated,
|
||||
document_content,
|
||||
user_prompt,
|
||||
rewrite_section,
|
||||
rewrite_section_prefix,
|
||||
rewrite_section_suffix,
|
||||
rewrite_section_with_edits,
|
||||
has_insertion,
|
||||
has_replacement,
|
||||
};
|
||||
|
||||
self.handlebars.lock().render("content_prompt", &context)
|
||||
@@ -297,4 +302,11 @@ impl PromptBuilder {
|
||||
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
|
||||
self.handlebars.lock().render("edit_workflow", &())
|
||||
}
|
||||
|
||||
pub fn generate_step_resolution_prompt(
|
||||
&self,
|
||||
context: &StepResolutionContext,
|
||||
) -> Result<String, RenderError> {
|
||||
self.handlebars.lock().render("step_resolution", context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ impl SlashCommandCompletionProvider {
|
||||
&command_name,
|
||||
&[],
|
||||
true,
|
||||
false,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
@@ -209,7 +208,6 @@ impl SlashCommandCompletionProvider {
|
||||
&command_name,
|
||||
&completed_arguments,
|
||||
true,
|
||||
false,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{
|
||||
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
|
||||
SlashCommandOutputSection,
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use context_servers::{
|
||||
@@ -9,10 +8,9 @@ use context_servers::{
|
||||
protocol::PromptInfo,
|
||||
};
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use text::LineEnding;
|
||||
use ui::{IconName, SharedString};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -44,64 +42,20 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
self.prompt.arguments.as_ref().map_or(false, |args| {
|
||||
args.iter().any(|arg| arg.required == Some(true))
|
||||
})
|
||||
self.prompt
|
||||
.arguments
|
||||
.as_ref()
|
||||
.map_or(false, |args| !args.is_empty())
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let server_id = self.server_id.clone();
|
||||
let prompt_name = self.prompt.name.clone();
|
||||
let manager = ContextServerManager::global(cx);
|
||||
let manager = manager.read(cx);
|
||||
|
||||
let (arg_name, arg_val) = match completion_argument(&self.prompt, arguments) {
|
||||
Ok(tp) => tp,
|
||||
Err(e) => {
|
||||
return Task::ready(Err(e));
|
||||
}
|
||||
};
|
||||
if let Some(server) = manager.get_server(&server_id) {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let Some(protocol) = server.client.read().clone() else {
|
||||
return Err(anyhow!("Context server not initialized"));
|
||||
};
|
||||
|
||||
let completion_result = protocol
|
||||
.completion(
|
||||
context_servers::types::CompletionReference::Prompt(
|
||||
context_servers::types::PromptReference {
|
||||
r#type: context_servers::types::PromptReferenceType::Prompt,
|
||||
name: prompt_name,
|
||||
},
|
||||
),
|
||||
arg_name,
|
||||
arg_val,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let completions = completion_result
|
||||
.values
|
||||
.into_iter()
|
||||
.map(|value| ArgumentCompletion {
|
||||
label: CodeLabel::plain(value.clone(), None),
|
||||
new_text: value,
|
||||
after_completion: AfterCompletion::Continue,
|
||||
replace_previous_arguments: false,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(completions)
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("Context server not found")))
|
||||
}
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
@@ -113,11 +67,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let server_id = self.server_id.clone();
|
||||
let prompt_name = self.prompt.name.clone();
|
||||
|
||||
let prompt_args = match prompt_arguments(&self.prompt, arguments) {
|
||||
Ok(args) => args,
|
||||
Err(e) => return Task::ready(Err(e)),
|
||||
};
|
||||
let argument = arguments.first().cloned();
|
||||
|
||||
let manager = ContextServerManager::global(cx);
|
||||
let manager = manager.read(cx);
|
||||
@@ -126,23 +76,18 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
let Some(protocol) = server.client.read().clone() else {
|
||||
return Err(anyhow!("Context server not initialized"));
|
||||
};
|
||||
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
|
||||
let mut prompt = result.prompt;
|
||||
|
||||
// We must normalize the line endings here, since servers might return CR characters.
|
||||
LineEnding::normalize(&mut prompt);
|
||||
let result = protocol
|
||||
.run_prompt(&prompt_name, prompt_arguments(&self.prompt, argument)?)
|
||||
.await?;
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..(prompt.len()),
|
||||
range: 0..result.len(),
|
||||
icon: IconName::ZedAssistant,
|
||||
label: SharedString::from(
|
||||
result
|
||||
.description
|
||||
.unwrap_or(format!("Result from {}", prompt_name)),
|
||||
),
|
||||
label: SharedString::from(format!("Result from {}", prompt_name)),
|
||||
}],
|
||||
text: prompt,
|
||||
text: result,
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
@@ -152,45 +97,19 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_argument(prompt: &PromptInfo, arguments: &[String]) -> Result<(String, String)> {
|
||||
if arguments.is_empty() {
|
||||
return Err(anyhow!("No arguments given"));
|
||||
}
|
||||
|
||||
fn prompt_arguments(
|
||||
prompt: &PromptInfo,
|
||||
argument: Option<String>,
|
||||
) -> Result<HashMap<String, String>> {
|
||||
match &prompt.arguments {
|
||||
Some(args) if args.len() == 1 => {
|
||||
let arg_name = args[0].name.clone();
|
||||
let arg_value = arguments.join(" ");
|
||||
Ok((arg_name, arg_value))
|
||||
}
|
||||
Some(_) => Err(anyhow!("Prompt must have exactly one argument")),
|
||||
None => Err(anyhow!("Prompt has no arguments")),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result<HashMap<String, String>> {
|
||||
match &prompt.arguments {
|
||||
Some(args) if args.len() > 1 => Err(anyhow!(
|
||||
Some(args) if args.len() >= 2 => Err(anyhow!(
|
||||
"Prompt has more than one argument, which is not supported"
|
||||
)),
|
||||
Some(args) if args.len() == 1 => {
|
||||
if !arguments.is_empty() {
|
||||
let mut map = HashMap::default();
|
||||
map.insert(args[0].name.clone(), arguments.join(" "));
|
||||
Ok(map)
|
||||
} else if arguments.is_empty() && args[0].required == Some(false) {
|
||||
Ok(HashMap::default())
|
||||
} else {
|
||||
Err(anyhow!("Prompt expects argument but none given"))
|
||||
}
|
||||
}
|
||||
Some(_) | None => {
|
||||
if arguments.is_empty() {
|
||||
Ok(HashMap::default())
|
||||
} else {
|
||||
Err(anyhow!("Prompt expects no arguments but some were given"))
|
||||
}
|
||||
}
|
||||
Some(args) if args.len() == 1 => match argument {
|
||||
Some(value) => Ok(HashMap::from_iter([(args[0].name.clone(), value)])),
|
||||
None => Err(anyhow!("Prompt expects argument but none given")),
|
||||
},
|
||||
Some(_) | None => Ok(HashMap::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +119,7 @@ fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result<HashMap
|
||||
pub fn acceptable_prompt(prompt: &PromptInfo) -> bool {
|
||||
match &prompt.arguments {
|
||||
None => true,
|
||||
Some(args) if args.len() <= 1 => true,
|
||||
Some(args) if args.len() == 1 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,11 @@ impl SlashCommand for FileSlashCommand {
|
||||
Some(ArgumentCompletion {
|
||||
label,
|
||||
new_text: text,
|
||||
after_completion: AfterCompletion::Compose,
|
||||
after_completion: if path_match.is_dir {
|
||||
AfterCompletion::Compose
|
||||
} else {
|
||||
AfterCompletion::Run
|
||||
},
|
||||
replace_previous_arguments: false,
|
||||
})
|
||||
})
|
||||
@@ -508,6 +512,10 @@ mod custom_path_matcher {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sources(&self) -> &[String] {
|
||||
&self.sources
|
||||
}
|
||||
|
||||
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
|
||||
let other_path = other.as_ref();
|
||||
self.sources
|
||||
|
||||
@@ -9,13 +9,13 @@ use collections::{HashMap, HashSet};
|
||||
use editor::Editor;
|
||||
use futures::future::join_all;
|
||||
use gpui::{Entity, Task, WeakView};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{ActiveTheme, WindowContext};
|
||||
use ui::WindowContext;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct TabSlashCommand;
|
||||
@@ -79,8 +79,6 @@ impl SlashCommand for TabSlashCommand {
|
||||
let current_query = arguments.last().cloned().unwrap_or_default();
|
||||
let tab_items_search =
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
|
||||
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
cx.spawn(|_| async move {
|
||||
let tab_items = tab_items_search.await?;
|
||||
let run_command = tab_items.len() == 1;
|
||||
@@ -92,9 +90,8 @@ impl SlashCommand for TabSlashCommand {
|
||||
if active_item_path.is_some() && active_item_path == path {
|
||||
return None;
|
||||
}
|
||||
let label = create_tab_completion_label(path.as_ref()?, comment_id);
|
||||
Some(ArgumentCompletion {
|
||||
label,
|
||||
label: path_string.clone().into(),
|
||||
new_text: path_string,
|
||||
replace_previous_arguments: false,
|
||||
after_completion: run_command.into(),
|
||||
@@ -103,17 +100,14 @@ impl SlashCommand for TabSlashCommand {
|
||||
|
||||
let active_item_completion = active_item_path
|
||||
.as_deref()
|
||||
.map(|active_item_path| {
|
||||
let path_string = active_item_path.to_string_lossy().to_string();
|
||||
let label = create_tab_completion_label(active_item_path, comment_id);
|
||||
ArgumentCompletion {
|
||||
label,
|
||||
new_text: path_string,
|
||||
replace_previous_arguments: false,
|
||||
after_completion: run_command.into(),
|
||||
}
|
||||
})
|
||||
.filter(|completion| !argument_set.contains(&completion.new_text));
|
||||
.map(|active_item_path| active_item_path.to_string_lossy().to_string())
|
||||
.filter(|path_string| !argument_set.contains(path_string))
|
||||
.map(|path_string| ArgumentCompletion {
|
||||
label: path_string.clone().into(),
|
||||
new_text: path_string,
|
||||
replace_previous_arguments: false,
|
||||
after_completion: run_command.into(),
|
||||
});
|
||||
|
||||
Ok(active_item_completion
|
||||
.into_iter()
|
||||
@@ -203,7 +197,6 @@ fn tab_items_for_queries(
|
||||
}
|
||||
|
||||
let mut timestamps_by_entity_id = HashMap::default();
|
||||
let mut visited_buffers = HashSet::default();
|
||||
let mut open_buffers = Vec::new();
|
||||
|
||||
for pane in workspace.panes() {
|
||||
@@ -218,11 +211,9 @@ fn tab_items_for_queries(
|
||||
if let Some(timestamp) =
|
||||
timestamps_by_entity_id.get(&editor.entity_id())
|
||||
{
|
||||
if visited_buffers.insert(buffer.read(cx).remote_id()) {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let full_path = snapshot.resolve_file_path(cx, true);
|
||||
open_buffers.push((full_path, snapshot, *timestamp));
|
||||
}
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let full_path = snapshot.resolve_file_path(cx, true);
|
||||
open_buffers.push((full_path, snapshot, *timestamp));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +244,7 @@ fn tab_items_for_queries(
|
||||
.fold(HashMap::default(), |mut candidates, (id, path_string)| {
|
||||
candidates
|
||||
.entry(path_string)
|
||||
.or_insert_with(Vec::new)
|
||||
.or_insert_with(|| Vec::new())
|
||||
.push(id);
|
||||
candidates
|
||||
});
|
||||
@@ -325,23 +316,3 @@ fn active_item_buffer(
|
||||
.snapshot();
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn create_tab_completion_label(
|
||||
path: &std::path::Path,
|
||||
comment_id: Option<HighlightId>,
|
||||
) -> CodeLabel {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
let parent_path = path
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(&file_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(&parent_path, comment_id);
|
||||
label.filter_range = 0..file_name.len();
|
||||
label
|
||||
}
|
||||
|
||||
@@ -1,72 +1,64 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use gpui::AnyElement;
|
||||
use gpui::DismissEvent;
|
||||
use gpui::WeakView;
|
||||
use picker::PickerEditorPosition;
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::ListItemSpacing;
|
||||
|
||||
use gpui::SharedString;
|
||||
use gpui::Task;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
|
||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
|
||||
pub struct SlashCommandSelector<T: PopoverTrigger> {
|
||||
handle: Option<PopoverMenuHandle<Picker<SlashCommandDelegate>>>,
|
||||
registry: Arc<SlashCommandRegistry>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
trigger: T,
|
||||
info_text: Option<SharedString>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SlashCommandInfo {
|
||||
name: SharedString,
|
||||
description: SharedString,
|
||||
args: Option<SharedString>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SlashCommandEntry {
|
||||
Info(SlashCommandInfo),
|
||||
Advert {
|
||||
name: SharedString,
|
||||
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
||||
on_confirm: fn(&mut WindowContext<'_>),
|
||||
},
|
||||
}
|
||||
|
||||
impl AsRef<str> for SlashCommandEntry {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
|
||||
| SlashCommandEntry::Advert { name, .. } => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandDelegate {
|
||||
all_commands: Vec<SlashCommandEntry>,
|
||||
filtered_commands: Vec<SlashCommandEntry>,
|
||||
pub struct SlashCommandDelegate {
|
||||
all_commands: Vec<SlashCommandInfo>,
|
||||
filtered_commands: Vec<SlashCommandInfo>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> SlashCommandSelector<T> {
|
||||
pub(crate) fn new(
|
||||
pub fn new(
|
||||
registry: Arc<SlashCommandRegistry>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
trigger: T,
|
||||
) -> Self {
|
||||
SlashCommandSelector {
|
||||
handle: None,
|
||||
registry,
|
||||
active_context_editor,
|
||||
trigger,
|
||||
info_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<SlashCommandDelegate>>) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.info_text = Some(text.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for SlashCommandDelegate {
|
||||
@@ -102,7 +94,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
model_info
|
||||
.as_ref()
|
||||
.name
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
@@ -120,42 +112,13 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn separators_after_indices(&self) -> Vec<usize> {
|
||||
let mut ret = vec![];
|
||||
let mut previous_is_advert = false;
|
||||
|
||||
for (index, command) in self.filtered_commands.iter().enumerate() {
|
||||
if previous_is_advert {
|
||||
if let SlashCommandEntry::Info(_) = command {
|
||||
previous_is_advert = false;
|
||||
debug_assert_ne!(
|
||||
index, 0,
|
||||
"index cannot be zero, as we can never have a separator at 0th position"
|
||||
);
|
||||
ret.push(index - 1);
|
||||
}
|
||||
} else {
|
||||
if let SlashCommandEntry::Advert { .. } = command {
|
||||
previous_is_advert = true;
|
||||
if index != 0 {
|
||||
ret.push(index - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
||||
if let SlashCommandEntry::Info(info) = command {
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
context_editor.insert_command(&info.name, cx)
|
||||
})
|
||||
.ok();
|
||||
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
|
||||
on_confirm(cx);
|
||||
}
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
context_editor.insert_command(&command.name, cx)
|
||||
})
|
||||
.ok();
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
@@ -170,63 +133,30 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
_: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let command_info = self.filtered_commands.get(ix)?;
|
||||
|
||||
match command_info {
|
||||
SlashCommandEntry::Info(info) => Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.group(format!("command-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.min_w(px(220.))
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(
|
||||
h_flex().w_full().min_w(px(220.)).child(
|
||||
v_flex()
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(div().font_buffer(cx).child({
|
||||
let mut label = format!("/{}", info.name);
|
||||
if let Some(args) =
|
||||
info.args.as_ref().filter(|_| selected)
|
||||
{
|
||||
label.push_str(&args);
|
||||
}
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
}))
|
||||
.children(info.args.clone().filter(|_| !selected).map(
|
||||
|args| {
|
||||
div()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args).size(LabelSize::Small),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
))
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
Label::new(format!("/{}", command_info.name))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(command_info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
),
|
||||
SlashCommandEntry::Advert { renderer, .. } => Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(renderer(cx)),
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,41 +169,11 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
.filter_map(|command_name| {
|
||||
let command = self.registry.command(&command_name)?;
|
||||
let menu_text = SharedString::from(Arc::from(command.menu_text()));
|
||||
let label = command.label(cx);
|
||||
let args = label.filter_range.end.ne(&label.text.len()).then(|| {
|
||||
SharedString::from(
|
||||
label.text[label.filter_range.end..label.text.len()].to_owned(),
|
||||
)
|
||||
});
|
||||
Some(SlashCommandEntry::Info(SlashCommandInfo {
|
||||
Some(SlashCommandInfo {
|
||||
name: command_name.into(),
|
||||
description: menu_text,
|
||||
args,
|
||||
}))
|
||||
})
|
||||
})
|
||||
.chain([SlashCommandEntry::Advert {
|
||||
name: "create-your-command".into(),
|
||||
renderer: |cx| {
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.font_buffer(cx)
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().font_buffer(cx).child(
|
||||
Label::new("create-your-command").size(LabelSize::Small),
|
||||
))
|
||||
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(
|
||||
Label::new("Learn how to create a custom command")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||
}])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let delegate = SlashCommandDelegate {
|
||||
@@ -288,10 +188,6 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
picker
|
||||
});
|
||||
|
||||
let handle = self
|
||||
.active_context_editor
|
||||
.update(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
.trigger(self.trigger)
|
||||
@@ -301,6 +197,5 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.when_some(handle, |this, handle| this.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,11 +157,11 @@ impl TerminalInlineAssistant {
|
||||
PromptEditorEvent::StopRequested => {
|
||||
self.stop_assist(assist_id, cx);
|
||||
}
|
||||
PromptEditorEvent::ConfirmRequested { execute } => {
|
||||
self.finish_assist(assist_id, false, *execute, cx);
|
||||
PromptEditorEvent::ConfirmRequested => {
|
||||
self.finish_assist(assist_id, false, cx);
|
||||
}
|
||||
PromptEditorEvent::CancelRequested => {
|
||||
self.finish_assist(assist_id, true, false, cx);
|
||||
self.finish_assist(assist_id, true, cx);
|
||||
}
|
||||
PromptEditorEvent::DismissRequested => {
|
||||
self.dismiss_assist(assist_id, cx);
|
||||
@@ -282,7 +282,6 @@ impl TerminalInlineAssistant {
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: 1.0,
|
||||
})
|
||||
@@ -292,7 +291,6 @@ impl TerminalInlineAssistant {
|
||||
&mut self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
undo: bool,
|
||||
execute: bool,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.dismiss_assist(assist_id, cx);
|
||||
@@ -308,7 +306,7 @@ impl TerminalInlineAssistant {
|
||||
assist.codegen.update(cx, |codegen, cx| {
|
||||
if undo {
|
||||
codegen.undo(cx);
|
||||
} else if execute {
|
||||
} else {
|
||||
codegen.complete(cx);
|
||||
}
|
||||
});
|
||||
@@ -424,7 +422,7 @@ impl TerminalInlineAssist {
|
||||
}
|
||||
|
||||
if assist.prompt_editor.is_none() {
|
||||
this.finish_assist(assist_id, false, false, cx);
|
||||
this.finish_assist(assist_id, false, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -437,7 +435,7 @@ impl TerminalInlineAssist {
|
||||
enum PromptEditorEvent {
|
||||
StartRequested,
|
||||
StopRequested,
|
||||
ConfirmRequested { execute: bool },
|
||||
ConfirmRequested,
|
||||
CancelRequested,
|
||||
DismissRequested,
|
||||
Resized { height_in_lines: u8 },
|
||||
@@ -465,8 +463,7 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let status = &self.codegen.read(cx).status;
|
||||
let buttons = match status {
|
||||
let buttons = match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
@@ -511,16 +508,15 @@ impl Render for PromptEditor {
|
||||
]
|
||||
}
|
||||
CodegenStatus::Error(_) | CodegenStatus::Done => {
|
||||
let cancel = IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)));
|
||||
|
||||
let has_error = matches!(status, CodegenStatus::Error(_));
|
||||
if has_error || self.edited_since_done {
|
||||
vec![
|
||||
cancel,
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
),
|
||||
if self.edited_since_done {
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -534,35 +530,19 @@ impl Render for PromptEditor {
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
})),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
cancel,
|
||||
IconButton::new("accept", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
||||
})),
|
||||
}))
|
||||
} else {
|
||||
IconButton::new("confirm", IconName::Play)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Execute Generated Command",
|
||||
&menu::SecondaryConfirm,
|
||||
cx,
|
||||
)
|
||||
Tooltip::for_action("Execute generated command", &menu::Confirm, cx)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
|
||||
})),
|
||||
]
|
||||
}
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested);
|
||||
}))
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -574,7 +554,6 @@ impl Render for PromptEditor {
|
||||
.h_full()
|
||||
.w_full()
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::secondary_confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::move_down))
|
||||
@@ -625,7 +604,7 @@ impl Render for PromptEditor {
|
||||
.child(div().flex_1().child(self.render_prompt_editor(cx)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.gap_2()
|
||||
.pr_4()
|
||||
.children(self.render_token_count(cx))
|
||||
.children(buttons),
|
||||
@@ -825,22 +804,13 @@ impl PromptEditor {
|
||||
CodegenStatus::Pending => {
|
||||
cx.emit(PromptEditorEvent::DismissRequested);
|
||||
}
|
||||
CodegenStatus::Done => {
|
||||
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
if self.edited_since_done {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
} else {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested);
|
||||
}
|
||||
}
|
||||
CodegenStatus::Error(_) => {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -990,7 +960,7 @@ impl TerminalTransaction {
|
||||
|
||||
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
|
||||
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
|
||||
let input = Self::sanitize_input(hunk);
|
||||
let input = hunk.replace(CARRIAGE_RETURN, " ");
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(input));
|
||||
}
|
||||
@@ -1005,10 +975,6 @@ impl TerminalTransaction {
|
||||
terminal.input(CARRIAGE_RETURN.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn sanitize_input(input: String) -> String {
|
||||
input.replace(['\r', '\n'], "")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
@@ -1040,7 +1006,7 @@ impl Codegen {
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
self.generation = cx.spawn(|this, mut cx| async move {
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let response = model.stream_completion_text(prompt, &cx).await;
|
||||
let response = model.stream_completion(prompt, &cx).await;
|
||||
let generate = async {
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod now_tool;
|
||||
@@ -1,60 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Timezone {
|
||||
/// Use UTC for the datetime.
|
||||
Utc,
|
||||
/// Use local time for the datetime.
|
||||
Local,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FileToolInput {
|
||||
/// The timezone to use for the datetime.
|
||||
timezone: Timezone,
|
||||
}
|
||||
|
||||
pub struct NowTool;
|
||||
|
||||
impl Tool for NowTool {
|
||||
fn name(&self) -> String {
|
||||
"now".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Returns the current datetime in RFC 3339 format.".into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(FileToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_workspace: WeakView<workspace::Workspace>,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<String>> {
|
||||
let input: FileToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let now = match input.timezone {
|
||||
Timezone::Utc => Utc::now().to_rfc3339(),
|
||||
Timezone::Local => Local::now().to_rfc3339(),
|
||||
};
|
||||
let text = format!("The current datetime is {now}.");
|
||||
|
||||
Task::ready(Ok(text))
|
||||
}
|
||||
}
|
||||
315
crates/assistant/src/workflow/step_view.rs
Normal file
@@ -0,0 +1,315 @@
|
||||
use super::WorkflowStep;
|
||||
use crate::{Assist, Context};
|
||||
use editor::{
|
||||
display_map::{BlockDisposition, BlockProperties, BlockStyle},
|
||||
Editor, EditorEvent, ExcerptRange, MultiBuffer,
|
||||
};
|
||||
use gpui::{
|
||||
div, AnyElement, AppContext, Context as _, Empty, EventEmitter, FocusableView, IntoElement,
|
||||
Model, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
|
||||
VisualContext as _, WeakModel, WindowContext,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Anchor, Buffer, LanguageRegistry};
|
||||
use std::{ops::DerefMut, sync::Arc};
|
||||
use text::OffsetRangeExt;
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{
|
||||
h_flex, v_flex, ButtonCommon as _, ButtonLike, ButtonStyle, Color, Icon, IconName,
|
||||
InteractiveElement as _, Label, LabelCommon as _,
|
||||
};
|
||||
use workspace::{
|
||||
item::{self, Item},
|
||||
pane,
|
||||
searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
pub struct WorkflowStepView {
|
||||
step: WeakModel<WorkflowStep>,
|
||||
tool_output_buffer: Model<Buffer>,
|
||||
editor: View<Editor>,
|
||||
}
|
||||
|
||||
impl WorkflowStepView {
|
||||
pub fn new(
|
||||
context: Model<Context>,
|
||||
step: Model<WorkflowStep>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let tool_output_buffer =
|
||||
cx.new_model(|cx| Buffer::local(step.read(cx).tool_output.clone(), cx));
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
|
||||
buffer.push_excerpts(
|
||||
context.read(cx).buffer().clone(),
|
||||
[ExcerptRange {
|
||||
context: step.read(cx).context_buffer_range.clone(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buffer.push_excerpts(
|
||||
tool_output_buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: Anchor::MIN..Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buffer
|
||||
});
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
|
||||
let input_start_anchor = multi_buffer::Anchor::min();
|
||||
let output_start_anchor = buffer_snapshot
|
||||
.anchor_in_excerpt(output_excerpt, Anchor::MIN)
|
||||
.unwrap();
|
||||
let output_end_anchor = multi_buffer::Anchor::max();
|
||||
|
||||
let handle = cx.view().downgrade();
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(false);
|
||||
editor.insert_blocks(
|
||||
[
|
||||
BlockProperties {
|
||||
position: input_start_anchor,
|
||||
height: 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(|cx| section_header("Step Input", cx)),
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
},
|
||||
BlockProperties {
|
||||
position: output_start_anchor,
|
||||
height: 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(|cx| section_header("Tool Output", cx)),
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
},
|
||||
BlockProperties {
|
||||
position: output_end_anchor,
|
||||
height: 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(move |cx| {
|
||||
if let Some(result) = handle.upgrade().and_then(|this| {
|
||||
this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
|
||||
}) {
|
||||
v_flex()
|
||||
.child(section_header("Output", cx))
|
||||
.child(
|
||||
div().pl(cx.gutter_dimensions.full_width()).child(result),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Empty.into_any_element()
|
||||
}
|
||||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
priority: 0,
|
||||
},
|
||||
],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
});
|
||||
|
||||
cx.observe(&step, Self::step_updated).detach();
|
||||
cx.observe_release(&step, Self::step_released).detach();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Ok(language) = language_registry.language_for_name("JSON").await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.tool_output_buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(language), cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
tool_output_buffer,
|
||||
step: step.downgrade(),
|
||||
editor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn step(&self) -> &WeakModel<WorkflowStep> {
|
||||
&self.step
|
||||
}
|
||||
|
||||
fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
let step = self.step.upgrade()?;
|
||||
let result = step.read(cx).resolution.as_ref()?;
|
||||
match result {
|
||||
Ok(result) => {
|
||||
Some(
|
||||
v_flex()
|
||||
.child(result.title.clone())
|
||||
.children(result.suggestion_groups.iter().filter_map(
|
||||
|(buffer, suggestion_groups)| {
|
||||
let buffer = buffer.read(cx);
|
||||
let path = buffer.file().map(|f| f.path());
|
||||
let snapshot = buffer.snapshot();
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.border_b_1()
|
||||
.children(path.map(|path| format!("path: {}", path.display())))
|
||||
.children(suggestion_groups.iter().map(|group| {
|
||||
v_flex().pt_2().pl_2().children(
|
||||
group.suggestions.iter().map(|suggestion| {
|
||||
let range = suggestion.range().to_point(&snapshot);
|
||||
v_flex()
|
||||
.children(
|
||||
suggestion.description().map(|desc| {
|
||||
format!("description: {desc}")
|
||||
}),
|
||||
)
|
||||
.child(format!("kind: {}", suggestion.kind()))
|
||||
.children(suggestion.symbol_path().map(
|
||||
|path| format!("symbol path: {}", path.0),
|
||||
))
|
||||
.child(format!(
|
||||
"lines: {} - {}",
|
||||
range.start.row + 1,
|
||||
range.end.row + 1
|
||||
))
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.into()
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
Err(error) => Some(format!("{:?}", error).into_any_element()),
|
||||
}
|
||||
}
|
||||
|
||||
fn step_updated(&mut self, step: Model<WorkflowStep>, cx: &mut ViewContext<Self>) {
|
||||
self.tool_output_buffer.update(cx, |buffer, cx| {
|
||||
let text = step.read(cx).tool_output.clone();
|
||||
buffer.set_text(text, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn step_released(&mut self, _: &mut WorkflowStep, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(EditorEvent::Closed);
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
||||
self.step
|
||||
.update(cx, |step, cx| {
|
||||
step.resolve(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn section_header(
|
||||
name: &'static str,
|
||||
cx: &mut editor::display_map::BlockContext,
|
||||
) -> gpui::AnyElement {
|
||||
h_flex()
|
||||
.pl(cx.gutter_dimensions.full_width())
|
||||
.h_11()
|
||||
.w_full()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(
|
||||
ButtonLike::new("role")
|
||||
.style(ButtonStyle::Filled)
|
||||
.child(Label::new(name).color(Color::Default)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
impl Render for WorkflowStepView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.key_context("ContextEditor")
|
||||
.on_action(cx.listener(Self::resolve))
|
||||
.flex_grow()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(self.editor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for WorkflowStepView {}
|
||||
|
||||
impl FocusableView for WorkflowStepView {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for WorkflowStepView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
|
||||
let step = self.step.upgrade()?.read(cx);
|
||||
let context = step.context.upgrade()?.read(cx);
|
||||
let buffer = context.buffer().read(cx);
|
||||
let index = context
|
||||
.workflow_step_index_for_range(&step.context_buffer_range, buffer)
|
||||
.ok()?
|
||||
+ 1;
|
||||
Some(format!("Step {index}").into())
|
||||
}
|
||||
|
||||
fn tab_icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
|
||||
Some(Icon::new(IconName::Pencil))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } => {
|
||||
f(item::ItemEvent::Edit);
|
||||
}
|
||||
EditorEvent::TitleChanged => {
|
||||
f(item::ItemEvent::UpdateTab);
|
||||
}
|
||||
EditorEvent::Closed => f(item::ItemEvent::CloseItem),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::set_nav_history(editor, nav_history, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "assistant_tool"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_tool.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,35 +0,0 @@
|
||||
mod tool_registry;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use tool_registry::*;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
ToolRegistry::default_global(cx);
|
||||
}
|
||||
|
||||
/// A tool that can be used by a language model.
|
||||
pub trait Tool: 'static + Send + Sync {
|
||||
/// Returns the name of the tool.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the description of the tool.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the JSON schema that describes the tool's input.
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
serde_json::Value::Object(serde_json::Map::default())
|
||||
}
|
||||
|
||||
/// Runs the tool with the provided input.
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<String>>;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::Global;
|
||||
use gpui::{AppContext, ReadGlobal};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::Tool;
|
||||
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
struct GlobalToolRegistry(Arc<ToolRegistry>);
|
||||
|
||||
impl Global for GlobalToolRegistry {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ToolRegistryState {
|
||||
tools: HashMap<Arc<str>, Arc<dyn Tool>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ToolRegistry {
|
||||
state: RwLock<ToolRegistryState>,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
/// Returns the global [`ToolRegistry`].
|
||||
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||
GlobalToolRegistry::global(cx).0.clone()
|
||||
}
|
||||
|
||||
/// Returns the global [`ToolRegistry`].
|
||||
///
|
||||
/// Inserts a default [`ToolRegistry`] if one does not yet exist.
|
||||
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
|
||||
cx.default_global::<GlobalToolRegistry>().0.clone()
|
||||
}
|
||||
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
state: RwLock::new(ToolRegistryState {
|
||||
tools: HashMap::default(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers the provided [`Tool`].
|
||||
pub fn register_tool(&self, tool: impl Tool) {
|
||||
let mut state = self.state.write();
|
||||
let tool_name: Arc<str> = tool.name().into();
|
||||
state.tools.insert(tool_name, Arc::new(tool));
|
||||
}
|
||||
|
||||
/// Unregisters the provided [`Tool`].
|
||||
pub fn unregister_tool(&self, tool: impl Tool) {
|
||||
self.unregister_tool_by_name(tool.name().as_str())
|
||||
}
|
||||
|
||||
/// Unregisters the tool with the given name.
|
||||
pub fn unregister_tool_by_name(&self, tool_name: &str) {
|
||||
let mut state = self.state.write();
|
||||
state.tools.remove(tool_name);
|
||||
}
|
||||
|
||||
/// Returns the list of tools in the registry.
|
||||
pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
|
||||
self.state.read().tools.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Returns the [`Tool`] with the given name.
|
||||
pub fn tool(&self, name: &str) -> Option<Arc<dyn Tool>> {
|
||||
self.state.read().tools.get(name).cloned()
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ impl SoundRegistry {
|
||||
let bytes = self
|
||||
.assets
|
||||
.load(&path)?
|
||||
.map(Ok)
|
||||
.map(|asset| Ok(asset))
|
||||
.unwrap_or_else(|| Err(anyhow::anyhow!("No such asset available")))?
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
|
||||
@@ -33,7 +33,6 @@ impl Sound {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
_output_stream: Option<OutputStream>,
|
||||
output_handle: Option<OutputStreamHandle>,
|
||||
@@ -46,7 +45,10 @@ impl Global for GlobalAudio {}
|
||||
|
||||
impl Audio {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
Self {
|
||||
_output_stream: None,
|
||||
output_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
|
||||
|
||||
@@ -88,58 +88,27 @@ struct JsonRelease {
|
||||
url: String,
|
||||
}
|
||||
|
||||
struct MacOsUnmounter {
|
||||
mount_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Drop for MacOsUnmounter {
|
||||
fn drop(&mut self) {
|
||||
let unmount_output = std::process::Command::new("hdiutil")
|
||||
.args(["detach", "-force"])
|
||||
.arg(&self.mount_path)
|
||||
.output();
|
||||
|
||||
match unmount_output {
|
||||
Ok(output) if output.status.success() => {
|
||||
log::info!("Successfully unmounted the disk image");
|
||||
}
|
||||
Ok(output) => {
|
||||
log::error!(
|
||||
"Failed to unmount disk image: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("Error while trying to unmount disk image: {:?}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not to automatically check for updates.
|
||||
#[derive(Clone, Copy, JsonSchema, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
#[serde(transparent)]
|
||||
struct AutoUpdateSetting(bool);
|
||||
|
||||
impl Default for AutoUpdateSetting {
|
||||
fn default() -> Self {
|
||||
Self(true)
|
||||
}
|
||||
}
|
||||
/// Whether or not to automatically check for updates.
|
||||
///
|
||||
/// Default: true
|
||||
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
struct AutoUpdateSettingContent(bool);
|
||||
|
||||
impl Settings for AutoUpdateSetting {
|
||||
const KEY: Option<&'static str> = Some("auto_update");
|
||||
|
||||
type FileContent = Self;
|
||||
type FileContent = Option<AutoUpdateSettingContent>;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
let auto_update = [sources.release_channel, sources.user]
|
||||
.into_iter()
|
||||
.find_map(|value| value.copied())
|
||||
.unwrap_or(*sources.default);
|
||||
.find_map(|value| value.copied().flatten())
|
||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
|
||||
|
||||
Ok(auto_update)
|
||||
Ok(Self(auto_update.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +183,7 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(message) = env::var("ZED_UPDATE_EXPLANATION") {
|
||||
if let Some(message) = env::var("ZED_UPDATE_EXPLANATION").ok() {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
@@ -257,7 +226,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
|
||||
let url = &auto_updater
|
||||
.http_client
|
||||
.build_url(&format!("/releases/{release_channel}/{current_version}"));
|
||||
cx.open_url(url);
|
||||
cx.open_url(&url);
|
||||
}
|
||||
|
||||
None
|
||||
@@ -725,7 +694,7 @@ async fn install_release_linux(
|
||||
}
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(["-av", "--delete"])
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&from)
|
||||
.arg(&to)
|
||||
.output()
|
||||
@@ -757,10 +726,10 @@ async fn install_release_macos(
|
||||
|
||||
mounted_app_path.push("/");
|
||||
let output = Command::new("hdiutil")
|
||||
.args(["attach", "-nobrowse"])
|
||||
.args(&["attach", "-nobrowse"])
|
||||
.arg(&downloaded_dmg)
|
||||
.arg("-mountroot")
|
||||
.arg(temp_dir.path())
|
||||
.arg(&temp_dir.path())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
@@ -770,13 +739,8 @@ async fn install_release_macos(
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
// Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
|
||||
let _unmounter = MacOsUnmounter {
|
||||
mount_path: mount_path.clone(),
|
||||
};
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(["-av", "--delete"])
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
.output()
|
||||
@@ -788,5 +752,17 @@ async fn install_release_macos(
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["detach"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to unount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(running_app_path)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,6 @@ pub struct Breadcrumbs {
|
||||
subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Default for Breadcrumbs {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -529,13 +529,14 @@ mod test {
|
||||
let (a, b) = cx.update(|cx| {
|
||||
(
|
||||
one_at_a_time.spawn(cx, |_| async {
|
||||
panic!("");
|
||||
assert!(false);
|
||||
Ok(2)
|
||||
}),
|
||||
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(a.await.unwrap(), None::<u32>);
|
||||
assert_eq!(a.await.unwrap(), None);
|
||||
assert_eq!(b.await.unwrap(), Some(3));
|
||||
|
||||
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
|
||||
|
||||
@@ -4,20 +4,30 @@ use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CallSettings {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
pub mute_on_join: bool,
|
||||
/// Whether your current project should be shared when joining an empty channel.
|
||||
pub share_on_join: bool,
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
/// Default: false
|
||||
pub mute_on_join: Option<bool>,
|
||||
|
||||
/// Whether your current project should be shared when joining an empty channel.
|
||||
///
|
||||
/// Default: true
|
||||
pub share_on_join: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for CallSettings {
|
||||
const KEY: Option<&'static str> = Some("calls");
|
||||
|
||||
type FileContent = Self;
|
||||
type FileContent = CallSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
|
||||
@@ -259,13 +259,13 @@ impl Room {
|
||||
None
|
||||
};
|
||||
|
||||
let did_join = room
|
||||
match room
|
||||
.update(&mut cx, |room, cx| {
|
||||
room.leave_when_empty = true;
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})?
|
||||
.await;
|
||||
match did_join {
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(room),
|
||||
Err(error) => Err(error.context("room creation failed")),
|
||||
}
|
||||
@@ -493,7 +493,7 @@ impl Room {
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade() {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
@@ -933,7 +933,7 @@ impl Room {
|
||||
let list = this
|
||||
.follows_by_leader_id_project_id
|
||||
.entry((leader, project_id))
|
||||
.or_default();
|
||||
.or_insert(Vec::new());
|
||||
if !list.contains(&follower) {
|
||||
list.push(follower);
|
||||
}
|
||||
@@ -942,7 +942,7 @@ impl Room {
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
this.leave(cx).detach();
|
||||
let _ = this.leave(cx).detach();
|
||||
}
|
||||
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
@@ -1017,11 +1017,19 @@ impl Room {
|
||||
.collect::<Vec<u64>>();
|
||||
speaker_ids.sort_unstable();
|
||||
for (sid, participant) in &mut self.remote_participants {
|
||||
participant.speaking = speaker_ids.binary_search(sid).is_ok();
|
||||
if let Ok(_) = speaker_ids.binary_search(sid) {
|
||||
participant.speaking = true;
|
||||
} else {
|
||||
participant.speaking = false;
|
||||
}
|
||||
}
|
||||
if let Some(id) = self.client.user_id() {
|
||||
if let Some(room) = &mut self.live_kit {
|
||||
room.speaking = speaker_ids.binary_search(&id).is_ok();
|
||||
if let Ok(_) = speaker_ids.binary_search(&id) {
|
||||
room.speaking = true;
|
||||
} else {
|
||||
room.speaking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@ mod channel_store_tests;
|
||||
|
||||
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
channel_store::init(client, user_store, cx);
|
||||
channel_buffer::init(&client.clone().into());
|
||||
channel_chat::init(&client.clone().into());
|
||||
channel_buffer::init(client);
|
||||
channel_chat::init(client);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
|
||||
use language::proto::serialize_version;
|
||||
use rpc::{
|
||||
proto::{self, AnyProtoClient, PeerId},
|
||||
proto::{self, PeerId},
|
||||
TypedEnvelope,
|
||||
};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
@@ -14,7 +14,7 @@ use util::ResultExt;
|
||||
|
||||
pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(crate) fn init(client: &AnyProtoClient) {
|
||||
pub(crate) fn init(client: &Arc<Client>) {
|
||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
|
||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
|
||||
}
|
||||
@@ -177,10 +177,13 @@ impl ChannelBuffer {
|
||||
match event {
|
||||
language::Event::Operation(operation) => {
|
||||
if *ZED_ALWAYS_ACTIVE {
|
||||
if let language::Operation::UpdateSelections { selections, .. } = operation {
|
||||
if selections.is_empty() {
|
||||
return;
|
||||
match operation {
|
||||
language::Operation::UpdateSelections { selections, .. } => {
|
||||
if selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let operation = language::proto::serialize_operation(operation);
|
||||
|
||||
@@ -11,7 +11,6 @@ use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::AnyProtoClient;
|
||||
use std::{
|
||||
ops::{ControlFlow, Range},
|
||||
sync::Arc,
|
||||
@@ -61,9 +60,9 @@ pub enum ChannelMessageId {
|
||||
Pending(usize),
|
||||
}
|
||||
|
||||
impl From<ChannelMessageId> for Option<u64> {
|
||||
fn from(val: ChannelMessageId) -> Self {
|
||||
match val {
|
||||
impl Into<Option<u64>> for ChannelMessageId {
|
||||
fn into(self) -> Option<u64> {
|
||||
match self {
|
||||
ChannelMessageId::Saved(id) => Some(id),
|
||||
ChannelMessageId::Pending(_) => None,
|
||||
}
|
||||
@@ -96,7 +95,7 @@ pub enum ChannelChatEvent {
|
||||
}
|
||||
|
||||
impl EventEmitter<ChannelChatEvent> for ChannelChat {}
|
||||
pub fn init(client: &AnyProtoClient) {
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
client.add_model_message_handler(ChannelChat::handle_message_sent);
|
||||
client.add_model_message_handler(ChannelChat::handle_message_removed);
|
||||
client.add_model_message_handler(ChannelChat::handle_message_updated);
|
||||
|
||||
@@ -249,14 +249,15 @@ impl ChannelStore {
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self) {
|
||||
if !self.did_subscribe
|
||||
&& self
|
||||
if !self.did_subscribe {
|
||||
if self
|
||||
.client
|
||||
.send(proto::SubscribeToChannels {})
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
self.did_subscribe = true;
|
||||
{
|
||||
self.did_subscribe = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +423,7 @@ impl ChannelStore {
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.or_insert_with(|| Default::default())
|
||||
.acknowledge_message_id(message_id);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -435,7 +436,7 @@ impl ChannelStore {
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.or_insert_with(|| Default::default())
|
||||
.update_latest_message_id(message_id);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -449,7 +450,7 @@ impl ChannelStore {
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.or_insert_with(|| Default::default())
|
||||
.acknowledge_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
}
|
||||
@@ -463,7 +464,7 @@ impl ChannelStore {
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.or_insert_with(|| Default::default())
|
||||
.update_latest_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
}
|
||||
@@ -923,7 +924,7 @@ impl ChannelStore {
|
||||
if let Some(role) = ChannelRole::from_i32(membership.role) {
|
||||
this.channel_states
|
||||
.entry(ChannelId(membership.channel_id))
|
||||
.or_default()
|
||||
.or_insert_with(|| ChannelState::default())
|
||||
.set_role(role)
|
||||
}
|
||||
}
|
||||
@@ -1093,7 +1094,11 @@ impl ChannelStore {
|
||||
id: ChannelId(channel.id),
|
||||
visibility: channel.visibility(),
|
||||
name: channel.name.into(),
|
||||
parent_path: channel.parent_path.into_iter().map(ChannelId).collect(),
|
||||
parent_path: channel
|
||||
.parent_path
|
||||
.into_iter()
|
||||
.map(|cid| ChannelId(cid))
|
||||
.collect(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
@@ -1108,11 +1113,14 @@ impl ChannelStore {
|
||||
|
||||
if channels_changed {
|
||||
if !payload.delete_channels.is_empty() {
|
||||
let delete_channels: Vec<ChannelId> =
|
||||
payload.delete_channels.into_iter().map(ChannelId).collect();
|
||||
let delete_channels: Vec<ChannelId> = payload
|
||||
.delete_channels
|
||||
.into_iter()
|
||||
.map(|cid| ChannelId(cid))
|
||||
.collect();
|
||||
self.channel_index.delete_channels(&delete_channels);
|
||||
self.channel_participants
|
||||
.retain(|channel_id, _| !delete_channels.contains(channel_id));
|
||||
.retain(|channel_id, _| !delete_channels.contains(&channel_id));
|
||||
|
||||
for channel_id in &delete_channels {
|
||||
let channel_id = *channel_id;
|
||||
|
||||
@@ -19,7 +19,6 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
collections.workspace = true
|
||||
ipc-channel = "0.18"
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
@@ -27,7 +26,6 @@ paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
util.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
exec.workspace = true
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use collections::HashMap;
|
||||
pub use ipc_channel::ipc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -16,7 +15,6 @@ pub enum CliRequest {
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
dev_server_token: Option<String>,
|
||||
env: Option<HashMap<String, String>>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
env, fs, io,
|
||||
@@ -12,7 +11,6 @@ use std::{
|
||||
sync::Arc,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::paths::PathWithPosition;
|
||||
|
||||
struct Detect;
|
||||
@@ -24,11 +22,7 @@ trait InstalledApp {
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "zed",
|
||||
disable_version_flag = true,
|
||||
after_help = "To read from stdin, append '-' (e.g. 'ps axf | zed -')"
|
||||
)]
|
||||
#[command(name = "zed", disable_version_flag = true)]
|
||||
struct Args {
|
||||
/// Wait for all of the given paths to be opened/closed before exiting.
|
||||
#[arg(short, long)]
|
||||
@@ -123,11 +117,9 @@ fn main() -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
let mut paths = vec![];
|
||||
let mut urls = vec![];
|
||||
let mut stdin_tmp_file: Option<fs::File> = None;
|
||||
for path in args.paths_with_position.iter() {
|
||||
if path.starts_with("zed://")
|
||||
|| path.starts_with("http://")
|
||||
@@ -136,11 +128,6 @@ fn main() -> Result<()> {
|
||||
|| path.starts_with("ssh://")
|
||||
{
|
||||
urls.push(path.to_string());
|
||||
} else if path == "-" && args.paths_with_position.len() == 1 {
|
||||
let file = NamedTempFile::new()?;
|
||||
paths.push(file.path().to_string_lossy().to_string());
|
||||
let (file, _) = file.keep()?;
|
||||
stdin_tmp_file = Some(file);
|
||||
} else {
|
||||
paths.push(parse_path_with_position(path)?)
|
||||
}
|
||||
@@ -151,14 +138,12 @@ fn main() -> Result<()> {
|
||||
move || {
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
urls,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
env,
|
||||
})?;
|
||||
|
||||
while let Ok(response) = rx.recv() {
|
||||
@@ -177,31 +162,11 @@ fn main() -> Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
let pipe_handle: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
|
||||
if let Some(mut tmp_file) = stdin_tmp_file {
|
||||
let mut stdin = std::io::stdin().lock();
|
||||
if io::IsTerminal::is_terminal(&stdin) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut buffer = [0; 8 * 1024];
|
||||
loop {
|
||||
let bytes_read = io::Read::read(&mut stdin, &mut buffer)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
io::Write::write(&mut tmp_file, &buffer[..bytes_read])?;
|
||||
}
|
||||
io::Write::flush(&mut tmp_file)?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if args.foreground {
|
||||
app.run_foreground(url)?;
|
||||
} else {
|
||||
app.launch(url)?;
|
||||
sender.join().unwrap()?;
|
||||
pipe_handle.join().unwrap()?;
|
||||
}
|
||||
|
||||
if let Some(exit_status) = exit_status.lock().take() {
|
||||
|
||||
@@ -27,6 +27,7 @@ fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
paths.workspace = true
|
||||
@@ -48,7 +49,6 @@ text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
mod socks;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use async_tungstenite::tungstenite::{
|
||||
client::IntoClientRequest,
|
||||
error::Error as WebsocketError,
|
||||
http::{HeaderValue, Request, StatusCode},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::oneshot, future::BoxFuture, AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt,
|
||||
TryFutureExt as _, TryStreamExt,
|
||||
channel::oneshot,
|
||||
future::{BoxFuture, LocalBoxFuture},
|
||||
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
|
||||
};
|
||||
use gpui::{actions, AppContext, AsyncAppContext, Global, Model, Task, WeakModel};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
use proto::{AnyProtoClient, EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet};
|
||||
use proto::ProtoClient;
|
||||
use rand::prelude::*;
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use socks::connect_socks_proxy_stream;
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
use std::{
|
||||
@@ -41,7 +43,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc, LazyLock, Weak,
|
||||
Arc, Weak,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -63,35 +65,27 @@ impl fmt::Display for DevServerToken {
|
||||
}
|
||||
}
|
||||
|
||||
static ZED_SERVER_URL: LazyLock<Option<String>> =
|
||||
LazyLock::new(|| std::env::var("ZED_SERVER_URL").ok());
|
||||
static ZED_RPC_URL: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("ZED_RPC_URL").ok());
|
||||
|
||||
/// An environment variable whose presence indicates that the development auth
|
||||
/// provider should be used.
|
||||
///
|
||||
/// Only works in development. Setting this environment variable in other release
|
||||
/// channels is a no-op.
|
||||
pub static ZED_DEVELOPMENT_AUTH: LazyLock<bool> = LazyLock::new(|| {
|
||||
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty())
|
||||
});
|
||||
pub static IMPERSONATE_LOGIN: LazyLock<Option<String>> = LazyLock::new(|| {
|
||||
std::env::var("ZED_IMPERSONATE")
|
||||
lazy_static! {
|
||||
static ref ZED_SERVER_URL: Option<String> = std::env::var("ZED_SERVER_URL").ok();
|
||||
static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
|
||||
/// An environment variable whose presence indicates that the development auth
|
||||
/// provider should be used.
|
||||
///
|
||||
/// Only works in development. Setting this environment variable in other release
|
||||
/// channels is a no-op.
|
||||
pub static ref ZED_DEVELOPMENT_AUTH: bool =
|
||||
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty());
|
||||
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
});
|
||||
|
||||
pub static ADMIN_API_TOKEN: LazyLock<Option<String>> = LazyLock::new(|| {
|
||||
std::env::var("ZED_ADMIN_API_TOKEN")
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
});
|
||||
|
||||
pub static ZED_APP_PATH: LazyLock<Option<PathBuf>> =
|
||||
LazyLock::new(|| std::env::var("ZED_APP_PATH").ok().map(PathBuf::from));
|
||||
|
||||
pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> =
|
||||
LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
pub static ref ZED_APP_PATH: Option<PathBuf> =
|
||||
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
||||
pub static ref ZED_ALWAYS_ACTIVE: bool =
|
||||
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty());
|
||||
}
|
||||
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
|
||||
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);
|
||||
@@ -99,68 +93,44 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct ClientSettings {
|
||||
/// The server to connect to. If the environment variable
|
||||
/// ZED_SERVER_URL is set, it will override this setting.
|
||||
pub server_url: String,
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ClientSettingsContent {
|
||||
server_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ClientSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
server_url: "https://zed.dev".to_owned(),
|
||||
}
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct ClientSettings {
|
||||
pub server_url: String,
|
||||
}
|
||||
|
||||
impl Settings for ClientSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = Self;
|
||||
type FileContent = ClientSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
let mut result = sources.json_merge::<Self>()?;
|
||||
if let Some(server_url) = &*ZED_SERVER_URL {
|
||||
result.server_url.clone_from(server_url)
|
||||
result.server_url.clone_from(&server_url)
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct ProxySettings {
|
||||
/// Set a proxy to use. The proxy protocol is specified by the URI scheme.
|
||||
///
|
||||
/// Supported URI scheme: `http`, `https`, `socks4`, `socks4a`, `socks5`,
|
||||
/// `socks5h`. `http` will be used when no scheme is specified.
|
||||
///
|
||||
/// By default no proxy will be used, or Zed will try get proxy settings from
|
||||
/// environment variables.
|
||||
///
|
||||
/// Examples:
|
||||
/// - "proxy": "socks5://localhost:10808"
|
||||
/// - "proxy": "http://127.0.0.1:10809"
|
||||
#[schemars(example = "Self::example_1")]
|
||||
#[schemars(example = "Self::example_2")]
|
||||
pub proxy: Option<String>,
|
||||
pub struct ProxySettingsContent {
|
||||
proxy: Option<String>,
|
||||
}
|
||||
|
||||
impl ProxySettings {
|
||||
fn example_1() -> String {
|
||||
"http://127.0.0.1:10809".to_owned()
|
||||
}
|
||||
fn example_2() -> String {
|
||||
"socks5://localhost:10808".to_owned()
|
||||
}
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ProxySettings {
|
||||
pub proxy: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for ProxySettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = Self;
|
||||
type FileContent = ProxySettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
Ok(Self {
|
||||
@@ -228,7 +198,6 @@ pub struct Client {
|
||||
telemetry: Arc<Telemetry>,
|
||||
credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static>,
|
||||
state: RwLock<ClientState>,
|
||||
handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -325,7 +294,30 @@ impl Status {
|
||||
struct ClientState {
|
||||
credentials: Option<Credentials>,
|
||||
status: (watch::Sender<Status>, watch::Receiver<Status>),
|
||||
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
|
||||
_reconnect_task: Option<Task<()>>,
|
||||
entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
|
||||
models_by_message_type: HashMap<TypeId, AnyWeakModel>,
|
||||
entity_types_by_message_type: HashMap<TypeId, TypeId>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
message_handlers: HashMap<
|
||||
TypeId,
|
||||
Arc<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ Fn(
|
||||
AnyModel,
|
||||
Box<dyn AnyTypedEnvelope>,
|
||||
&Arc<Client>,
|
||||
AsyncAppContext,
|
||||
) -> LocalBoxFuture<'static, Result<()>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
enum WeakSubscriber {
|
||||
Entity { handle: AnyWeakModel },
|
||||
Pending(Vec<Box<dyn AnyTypedEnvelope>>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -377,7 +369,12 @@ impl Default for ClientState {
|
||||
Self {
|
||||
credentials: None,
|
||||
status: watch::channel_with(Status::SignedOut),
|
||||
entity_id_extractors: Default::default(),
|
||||
_reconnect_task: None,
|
||||
models_by_message_type: Default::default(),
|
||||
entities_by_type_and_remote_id: Default::default(),
|
||||
entity_types_by_message_type: Default::default(),
|
||||
message_handlers: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,13 +395,13 @@ impl Drop for Subscription {
|
||||
match self {
|
||||
Subscription::Entity { client, id } => {
|
||||
if let Some(client) = client.upgrade() {
|
||||
let mut state = client.handler_set.lock();
|
||||
let mut state = client.state.write();
|
||||
let _ = state.entities_by_type_and_remote_id.remove(id);
|
||||
}
|
||||
}
|
||||
Subscription::Message { client, id } => {
|
||||
if let Some(client) = client.upgrade() {
|
||||
let mut state = client.handler_set.lock();
|
||||
let mut state = client.state.write();
|
||||
let _ = state.entity_types_by_message_type.remove(id);
|
||||
let _ = state.message_handlers.remove(id);
|
||||
}
|
||||
@@ -423,31 +420,22 @@ pub struct PendingEntitySubscription<T: 'static> {
|
||||
impl<T: 'static> PendingEntitySubscription<T> {
|
||||
pub fn set_model(mut self, model: &Model<T>, cx: &mut AsyncAppContext) -> Subscription {
|
||||
self.consumed = true;
|
||||
let mut handlers = self.client.handler_set.lock();
|
||||
let mut state = self.client.state.write();
|
||||
let id = (TypeId::of::<T>(), self.remote_id);
|
||||
let Some(EntityMessageSubscriber::Pending(messages)) =
|
||||
handlers.entities_by_type_and_remote_id.remove(&id)
|
||||
let Some(WeakSubscriber::Pending(messages)) =
|
||||
state.entities_by_type_and_remote_id.remove(&id)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
handlers.entities_by_type_and_remote_id.insert(
|
||||
state.entities_by_type_and_remote_id.insert(
|
||||
id,
|
||||
EntityMessageSubscriber::Entity {
|
||||
WeakSubscriber::Entity {
|
||||
handle: model.downgrade().into(),
|
||||
},
|
||||
);
|
||||
drop(handlers);
|
||||
drop(state);
|
||||
for message in messages {
|
||||
let client_id = self.client.id();
|
||||
let type_name = message.payload_type_name();
|
||||
let sender_id = message.original_sender_id();
|
||||
log::debug!(
|
||||
"handling queued rpc message. client_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
sender_id,
|
||||
type_name
|
||||
);
|
||||
self.client.handle_message(message, cx);
|
||||
}
|
||||
Subscription::Entity {
|
||||
@@ -460,8 +448,8 @@ impl<T: 'static> PendingEntitySubscription<T> {
|
||||
impl<T: 'static> Drop for PendingEntitySubscription<T> {
|
||||
fn drop(&mut self) {
|
||||
if !self.consumed {
|
||||
let mut state = self.client.handler_set.lock();
|
||||
if let Some(EntityMessageSubscriber::Pending(messages)) = state
|
||||
let mut state = self.client.state.write();
|
||||
if let Some(WeakSubscriber::Pending(messages)) = state
|
||||
.entities_by_type_and_remote_id
|
||||
.remove(&(TypeId::of::<T>(), self.remote_id))
|
||||
{
|
||||
@@ -542,7 +530,6 @@ impl Client {
|
||||
http,
|
||||
credentials_provider,
|
||||
state: Default::default(),
|
||||
handler_set: Default::default(),
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
authenticate: Default::default(),
|
||||
@@ -586,7 +573,10 @@ impl Client {
|
||||
pub fn teardown(&self) {
|
||||
let mut state = self.state.write();
|
||||
state._reconnect_task.take();
|
||||
self.handler_set.lock().clear();
|
||||
state.message_handlers.clear();
|
||||
state.models_by_message_type.clear();
|
||||
state.entities_by_type_and_remote_id.clear();
|
||||
state.entity_id_extractors.clear();
|
||||
self.peer.teardown();
|
||||
}
|
||||
|
||||
@@ -699,14 +689,14 @@ impl Client {
|
||||
{
|
||||
let id = (TypeId::of::<T>(), remote_id);
|
||||
|
||||
let mut state = self.handler_set.lock();
|
||||
let mut state = self.state.write();
|
||||
if state.entities_by_type_and_remote_id.contains_key(&id) {
|
||||
return Err(anyhow!("already subscribed to entity"));
|
||||
}
|
||||
|
||||
state
|
||||
.entities_by_type_and_remote_id
|
||||
.insert(id, EntityMessageSubscriber::Pending(Default::default()));
|
||||
.insert(id, WeakSubscriber::Pending(Default::default()));
|
||||
|
||||
Ok(PendingEntitySubscription {
|
||||
client: self.clone(),
|
||||
@@ -743,13 +733,13 @@ impl Client {
|
||||
E: 'static,
|
||||
H: 'static
|
||||
+ Sync
|
||||
+ Fn(Model<E>, TypedEnvelope<M>, AnyProtoClient, AsyncAppContext) -> F
|
||||
+ Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F
|
||||
+ Send
|
||||
+ Sync,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
let message_type_id = TypeId::of::<M>();
|
||||
let mut state = self.handler_set.lock();
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
.models_by_message_type
|
||||
.insert(message_type_id, entity.into());
|
||||
@@ -794,18 +784,85 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
where
|
||||
M: EntityMessage,
|
||||
E: 'static,
|
||||
H: 'static + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |subscriber, message, _, cx| {
|
||||
handler(subscriber.downcast::<E>().unwrap(), message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
where
|
||||
M: EntityMessage,
|
||||
E: 'static,
|
||||
H: 'static + Fn(AnyModel, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
let model_type_id = TypeId::of::<E>();
|
||||
let message_type_id = TypeId::of::<M>();
|
||||
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
.entity_types_by_message_type
|
||||
.insert(message_type_id, model_type_id);
|
||||
state
|
||||
.entity_id_extractors
|
||||
.entry(message_type_id)
|
||||
.or_insert_with(|| {
|
||||
|envelope| {
|
||||
envelope
|
||||
.as_any()
|
||||
.downcast_ref::<TypedEnvelope<M>>()
|
||||
.unwrap()
|
||||
.payload
|
||||
.remote_entity_id()
|
||||
}
|
||||
});
|
||||
let prev_handler = state.message_handlers.insert(
|
||||
message_type_id,
|
||||
Arc::new(move |handle, envelope, client, cx| {
|
||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
||||
handler(handle, *envelope, client.clone(), cx).boxed_local()
|
||||
}),
|
||||
);
|
||||
if prev_handler.is_some() {
|
||||
panic!("registered handler for the same message twice");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||
where
|
||||
M: EntityMessage + RequestMessage,
|
||||
E: 'static,
|
||||
H: 'static + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<M::Response>>,
|
||||
{
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |entity, envelope, client, cx| {
|
||||
Self::respond_to_request::<M, _>(
|
||||
envelope.receipt(),
|
||||
handler(entity.downcast::<E>().unwrap(), envelope, cx),
|
||||
client,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
|
||||
receipt: Receipt<T>,
|
||||
response: F,
|
||||
client: AnyProtoClient,
|
||||
client: Arc<Self>,
|
||||
) -> Result<()> {
|
||||
match response.await {
|
||||
Ok(response) => {
|
||||
client.send_response(receipt.message_id, response)?;
|
||||
client.respond(receipt, response)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
client.send_response(receipt.message_id, error.to_proto())?;
|
||||
client.respond_with_error(receipt, error.to_proto())?;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -1113,7 +1170,6 @@ impl Client {
|
||||
.unwrap_or_default();
|
||||
|
||||
let http = self.http.clone();
|
||||
let proxy = http.proxy().cloned();
|
||||
let credentials = credentials.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
cx.background_executor().spawn(async move {
|
||||
@@ -1135,7 +1191,7 @@ impl Client {
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
.ok_or_else(|| anyhow!("missing host in rpc url"))?;
|
||||
let stream = connect_socks_proxy_stream(proxy.as_ref(), rpc_host).await?;
|
||||
let stream = smol::net::TcpStream::connect(rpc_host).await?;
|
||||
|
||||
log::info!("connected to rpc endpoint {}", rpc_url);
|
||||
|
||||
@@ -1165,7 +1221,7 @@ impl Client {
|
||||
request_headers.insert("x-zed-app-version", HeaderValue::from_str(&app_version)?);
|
||||
request_headers.insert(
|
||||
"x-zed-release-channel",
|
||||
HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||
HeaderValue::from_str(&release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||
);
|
||||
|
||||
match url_scheme {
|
||||
@@ -1329,81 +1385,11 @@ impl Client {
|
||||
id: u64,
|
||||
}
|
||||
|
||||
let github_user = {
|
||||
#[derive(Deserialize)]
|
||||
struct GithubUser {
|
||||
id: i32,
|
||||
login: String,
|
||||
created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
let request = {
|
||||
let mut request_builder =
|
||||
Request::get(&format!("https://api.github.com/users/{login}"));
|
||||
if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {
|
||||
request_builder =
|
||||
request_builder.header("Authorization", format!("Bearer {}", github_token));
|
||||
}
|
||||
|
||||
request_builder.body(AsyncBody::empty())?
|
||||
};
|
||||
|
||||
let mut response = http
|
||||
.send(request)
|
||||
.await
|
||||
.context("error fetching GitHub user")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading GitHub user")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
serde_json::from_slice::<GithubUser>(body.as_slice()).map_err(|err| {
|
||||
log::error!("Error deserializing: {:?}", err);
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow!("error deserializing GitHub user")
|
||||
})?
|
||||
};
|
||||
|
||||
let query_params = [
|
||||
("github_login", &github_user.login),
|
||||
("github_user_id", &github_user.id.to_string()),
|
||||
(
|
||||
"github_user_created_at",
|
||||
&github_user.created_at.to_rfc3339(),
|
||||
),
|
||||
];
|
||||
|
||||
// Use the collab server's admin API to retrieve the ID
|
||||
// Use the collab server's admin API to retrieve the id
|
||||
// of the impersonated user.
|
||||
let mut url = self.rpc_url(http.clone(), None).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(
|
||||
&query_params
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
format!(
|
||||
"{}={}",
|
||||
key,
|
||||
url::form_urlencoded::byte_serialize(value.as_bytes()).collect::<String>()
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("&"),
|
||||
));
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request: http_client::Request<AsyncBody> = Request::get(url.as_str())
|
||||
.header("Authorization", format!("token {api_token}"))
|
||||
.body("".into())?;
|
||||
@@ -1430,7 +1416,7 @@ impl Client {
|
||||
|
||||
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||
self.state.write().credentials = None;
|
||||
self.disconnect(cx);
|
||||
self.disconnect(&cx);
|
||||
|
||||
if self.has_credentials(cx).await {
|
||||
self.credentials_provider
|
||||
@@ -1463,6 +1449,11 @@ impl Client {
|
||||
self.peer.send(self.connection_id()?, message)
|
||||
}
|
||||
|
||||
pub fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
let connection_id = self.connection_id()?;
|
||||
self.peer.send_dynamic(connection_id, envelope)
|
||||
}
|
||||
|
||||
pub fn request<T: RequestMessage>(
|
||||
&self,
|
||||
request: T,
|
||||
@@ -1544,56 +1535,115 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
|
||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
|
||||
self.peer.respond(receipt, response)
|
||||
}
|
||||
|
||||
fn respond_with_error<T: RequestMessage>(
|
||||
&self,
|
||||
receipt: Receipt<T>,
|
||||
error: proto::Error,
|
||||
) -> Result<()> {
|
||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
|
||||
self.peer.respond_with_error(receipt, error)
|
||||
}
|
||||
|
||||
fn handle_message(
|
||||
self: &Arc<Client>,
|
||||
message: Box<dyn AnyTypedEnvelope>,
|
||||
cx: &AsyncAppContext,
|
||||
) {
|
||||
let sender_id = message.sender_id();
|
||||
let request_id = message.message_id();
|
||||
let mut state = self.state.write();
|
||||
let type_name = message.payload_type_name();
|
||||
let original_sender_id = message.original_sender_id();
|
||||
let payload_type_id = message.payload_type_id();
|
||||
let sender_id = message.original_sender_id();
|
||||
|
||||
if let Some(future) = ProtoMessageHandlerSet::handle_message(
|
||||
&self.handler_set,
|
||||
message,
|
||||
self.clone().into(),
|
||||
cx.clone(),
|
||||
) {
|
||||
let mut subscriber = None;
|
||||
|
||||
if let Some(handle) = state
|
||||
.models_by_message_type
|
||||
.get(&payload_type_id)
|
||||
.and_then(|handle| handle.upgrade())
|
||||
{
|
||||
subscriber = Some(handle);
|
||||
} else if let Some((extract_entity_id, entity_type_id)) =
|
||||
state.entity_id_extractors.get(&payload_type_id).zip(
|
||||
state
|
||||
.entity_types_by_message_type
|
||||
.get(&payload_type_id)
|
||||
.copied(),
|
||||
)
|
||||
{
|
||||
let entity_id = (extract_entity_id)(message.as_ref());
|
||||
|
||||
match state
|
||||
.entities_by_type_and_remote_id
|
||||
.get_mut(&(entity_type_id, entity_id))
|
||||
{
|
||||
Some(WeakSubscriber::Pending(pending)) => {
|
||||
pending.push(message);
|
||||
return;
|
||||
}
|
||||
Some(weak_subscriber) => match weak_subscriber {
|
||||
WeakSubscriber::Entity { handle } => {
|
||||
subscriber = handle.upgrade();
|
||||
}
|
||||
|
||||
WeakSubscriber::Pending(_) => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let subscriber = if let Some(subscriber) = subscriber {
|
||||
subscriber
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
self.peer.respond_with_unhandled_message(message).log_err();
|
||||
return;
|
||||
};
|
||||
|
||||
let handler = state.message_handlers.get(&payload_type_id).cloned();
|
||||
// Dropping the state prevents deadlocks if the handler interacts with rpc::Client.
|
||||
// It also ensures we don't hold the lock while yielding back to the executor, as
|
||||
// that might cause the executor thread driving this future to block indefinitely.
|
||||
drop(state);
|
||||
|
||||
if let Some(handler) = handler {
|
||||
let future = handler(subscriber, message, self, cx.clone());
|
||||
let client_id = self.id();
|
||||
log::debug!(
|
||||
"rpc message received. client_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
original_sender_id,
|
||||
sender_id,
|
||||
type_name
|
||||
);
|
||||
cx.spawn(move |_| async move {
|
||||
match future.await {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
original_sender_id,
|
||||
type_name
|
||||
);
|
||||
match future.await {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
sender_id,
|
||||
type_name
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
|
||||
client_id,
|
||||
sender_id,
|
||||
type_name,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
|
||||
client_id,
|
||||
original_sender_id,
|
||||
type_name,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
self.peer
|
||||
.respond_with_unhandled_message(sender_id.into(), request_id, type_name)
|
||||
.log_err();
|
||||
self.peer.respond_with_unhandled_message(message).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1611,24 +1661,8 @@ impl ProtoClient for Client {
|
||||
self.request_dynamic(envelope, request_type).boxed()
|
||||
}
|
||||
|
||||
fn send(&self, envelope: proto::Envelope, message_type: &'static str) -> Result<()> {
|
||||
log::debug!("rpc send. client_id:{}, name:{}", self.id(), message_type);
|
||||
let connection_id = self.connection_id()?;
|
||||
self.peer.send_dynamic(connection_id, envelope)
|
||||
}
|
||||
|
||||
fn send_response(&self, envelope: proto::Envelope, message_type: &'static str) -> Result<()> {
|
||||
log::debug!(
|
||||
"rpc respond. client_id:{}, name:{}",
|
||||
self.id(),
|
||||
message_type
|
||||
);
|
||||
let connection_id = self.connection_id()?;
|
||||
self.peer.send_dynamic(connection_id, envelope)
|
||||
}
|
||||
|
||||
fn message_handler_set(&self) -> &parking_lot::Mutex<ProtoMessageHandlerSet> {
|
||||
&self.handler_set
|
||||
fn send(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
self.send_dynamic(envelope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1972,7 +2006,7 @@ mod tests {
|
||||
|
||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||
AnyProtoClient::from(client.clone()).add_model_message_handler(
|
||||
client.add_model_message_handler(
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
|
||||
match model.update(&mut cx, |model, _| model.id).unwrap() {
|
||||
1 => done_tx1.try_send(()).unwrap(),
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
//! socks proxy
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::io::{AsyncRead, AsyncWrite};
|
||||
use http_client::Uri;
|
||||
use tokio_socks::{
|
||||
io::Compat,
|
||||
tcp::{Socks4Stream, Socks5Stream},
|
||||
};
|
||||
|
||||
pub(crate) async fn connect_socks_proxy_stream(
|
||||
proxy: Option<&Uri>,
|
||||
rpc_host: (&str, u16),
|
||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
||||
let stream = match parse_socks_proxy(proxy) {
|
||||
Some((socks_proxy, SocksVersion::V4)) => {
|
||||
let stream = Socks4Stream::connect_with_socket(
|
||||
Compat::new(smol::net::TcpStream::connect(socks_proxy).await?),
|
||||
rpc_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error connecting to socks {}", err))?;
|
||||
Box::new(stream) as Box<dyn AsyncReadWrite>
|
||||
}
|
||||
Some((socks_proxy, SocksVersion::V5)) => Box::new(
|
||||
Socks5Stream::connect_with_socket(
|
||||
Compat::new(smol::net::TcpStream::connect(socks_proxy).await?),
|
||||
rpc_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error connecting to socks {}", err))?,
|
||||
) as Box<dyn AsyncReadWrite>,
|
||||
None => Box::new(smol::net::TcpStream::connect(rpc_host).await?) as Box<dyn AsyncReadWrite>,
|
||||
};
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn parse_socks_proxy(proxy: Option<&Uri>) -> Option<((String, u16), SocksVersion)> {
|
||||
let proxy_uri = proxy?;
|
||||
let scheme = proxy_uri.scheme_str()?;
|
||||
let socks_version = if scheme.starts_with("socks4") {
|
||||
// socks4
|
||||
SocksVersion::V4
|
||||
} else if scheme.starts_with("socks") {
|
||||
// socks, socks5
|
||||
SocksVersion::V5
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if let (Some(host), Some(port)) = (proxy_uri.host(), proxy_uri.port_u16()) {
|
||||
Some(((host.to_string(), port), socks_version))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// private helper structs and traits
|
||||
|
||||
enum SocksVersion {
|
||||
V4,
|
||||
V5,
|
||||
}
|
||||
|
||||
pub(crate) trait AsyncReadWrite: AsyncRead + AsyncWrite + Unpin + Send + 'static {}
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin + Send + 'static> AsyncReadWrite for T {}
|
||||
@@ -670,24 +670,6 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
|
||||
let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut summer = Sha256::new();
|
||||
summer.update(checksum_seed);
|
||||
summer.update(json);
|
||||
summer.update(checksum_seed);
|
||||
let mut checksum = String::new();
|
||||
for byte in summer.finalize().as_slice() {
|
||||
use std::fmt::Write;
|
||||
write!(&mut checksum, "{:02x}", byte).unwrap();
|
||||
}
|
||||
|
||||
Some(checksum)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -846,3 +828,21 @@ mod tests {
|
||||
&& telemetry.state.lock().first_event_date_time.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
|
||||
let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut summer = Sha256::new();
|
||||
summer.update(checksum_seed);
|
||||
summer.update(&json);
|
||||
summer.update(checksum_seed);
|
||||
let mut checksum = String::new();
|
||||
for byte in summer.finalize().as_slice() {
|
||||
use std::fmt::Write;
|
||||
write!(&mut checksum, "{:02x}", byte).unwrap();
|
||||
}
|
||||
|
||||
Some(checksum)
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ impl UserStore {
|
||||
cx.update(|cx| {
|
||||
if let Some(info) = info {
|
||||
let disable_staff = std::env::var("ZED_DISABLE_STAFF")
|
||||
.map_or(false, |v| !v.is_empty() && v != "0");
|
||||
.map_or(false, |v| v != "" && v != "0");
|
||||
let staff = info.staff && !disable_staff;
|
||||
cx.update_flags(staff, info.flags);
|
||||
client.telemetry.set_authenticated_user_info(
|
||||
|
||||
@@ -50,14 +50,14 @@ rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
rpc.workspace = true
|
||||
scrypt = "0.11"
|
||||
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
semantic_version.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
strum.workspace = true
|
||||
subtle.workspace = true
|
||||
rustc-demangle.workspace = true
|
||||
@@ -109,11 +109,11 @@ remote = { workspace = true, features = ["test-support"] }
|
||||
remote_server.workspace = true
|
||||
dev_server_projects.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] }
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
||||
serde_json.workspace = true
|
||||
session = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
sqlx = { version = "0.8", features = ["sqlite"] }
|
||||
sqlx = { version = "0.7", features = ["sqlite"] }
|
||||
theme.workspace = true
|
||||
unindent.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -139,11 +139,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: anthropic
|
||||
key: staff_api_key
|
||||
- name: LLM_CLOSED_BETA_MODEL_NAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: llm-closed-beta
|
||||
key: model_name
|
||||
- name: GOOGLE_AI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -216,12 +211,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: supermaven
|
||||
key: api_key
|
||||
- name: USER_BACKFILLER_GITHUB_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: user-backfiller
|
||||
key: github_access_token
|
||||
optional: true
|
||||
- name: INVITE_LINK_PREFIX
|
||||
value: ${INVITE_LINK_PREFIX}
|
||||
- name: RUST_BACKTRACE
|
||||
|
||||
@@ -9,14 +9,14 @@ CREATE TABLE "users" (
|
||||
"connected_once" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"metrics_id" TEXT,
|
||||
"github_user_id" INTEGER NOT NULL,
|
||||
"github_user_id" INTEGER,
|
||||
"accepted_tos_at" TIMESTAMP WITHOUT TIME ZONE,
|
||||
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
||||
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
||||
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
|
||||
CREATE UNIQUE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
|
||||
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
|
||||
|
||||
CREATE TABLE "access_tokens" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -86,7 +86,6 @@ CREATE TABLE "worktree_entries" (
|
||||
"is_ignored" BOOL NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
"git_status" INTEGER,
|
||||
"is_fifo" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
alter table users alter column github_user_id set not null;
|
||||
|
||||
drop index index_users_on_github_user_id;
|
||||
create unique index uix_users_on_github_user_id on users (github_user_id);
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE "worktree_entries"
|
||||
ADD "is_fifo" BOOL NOT NULL DEFAULT FALSE;
|
||||
@@ -1,4 +1,4 @@
|
||||
db-uri = "postgres://postgres@localhost/zed"
|
||||
server-port = 8081
|
||||
db-uri = "postgres://postgres@localhost/zed_llm"
|
||||
server-port = 8082
|
||||
jwt-secret = "the-postgrest-jwt-secret-for-authorization"
|
||||
log-level = "info"
|
||||
|
||||