Compare commits
1 Commits
debug-llm-
...
placeholde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22c7fe074e |
@@ -12,7 +12,3 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
# This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = ["--cfg", "windows_slim_errors"]
|
||||
|
||||
6
.github/pull_request_template.md
vendored
@@ -1,15 +1,13 @@
|
||||
Closes #ISSUE
|
||||
|
||||
|
||||
Release Notes:
|
||||
|
||||
- Added/Fixed/Improved ...
|
||||
- Added/Fixed/Improved ... ([#NNNNN](https://github.com/zed-industries/zed/issues/NNNNN)).
|
||||
|
||||
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
|
||||
|
||||
### Or...
|
||||
|
||||
Closes #ISSUE
|
||||
|
||||
Release Notes:
|
||||
|
||||
- N/A
|
||||
|
||||
13
.github/workflows/ci.yml
vendored
@@ -147,8 +147,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: cargo clippy
|
||||
# Windows can't run shell scripts, so we need to use `cargo xtask`.
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
@@ -231,20 +230,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@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
|
||||
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
|
||||
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
|
||||
@@ -319,7 +318,7 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
@@ -403,7 +402,7 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
|
||||
8
.github/workflows/deploy_collab.yml
vendored
@@ -106,12 +106,10 @@ jobs:
|
||||
export ZED_KUBE_NAMESPACE=production
|
||||
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=10
|
||||
export ZED_API_LOAD_BALANCER_SIZE_UNIT=2
|
||||
export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=2
|
||||
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
|
||||
export ZED_KUBE_NAMESPACE=staging
|
||||
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=1
|
||||
export ZED_API_LOAD_BALANCER_SIZE_UNIT=1
|
||||
export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=1
|
||||
else
|
||||
echo "cowardly refusing to deploy from an unknown branch"
|
||||
exit 1
|
||||
@@ -136,9 +134,3 @@ jobs:
|
||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
||||
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
||||
|
||||
export ZED_SERVICE_NAME=llm
|
||||
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_LLM_LOAD_BALANCER_SIZE_UNIT
|
||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
||||
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
||||
|
||||
24
.github/workflows/docs.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check_formatting:
|
||||
name: "Check formatting"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- run: pnpm dlx prettier . --check
|
||||
working-directory: ./docs
|
||||
3
.gitignore
vendored
@@ -29,6 +29,3 @@ DerivedData/
|
||||
.vscode
|
||||
.wrangler
|
||||
.flatpak-builder
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env.secret.toml
|
||||
|
||||
@@ -26,10 +26,6 @@
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"CSS": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"Rust": {
|
||||
"tasks": {
|
||||
"variables": {
|
||||
|
||||
1618
Cargo.lock
generated
93
Cargo.toml
@@ -1,11 +1,11 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_tooling",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -19,6 +19,7 @@ members = [
|
||||
"crates/collections",
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/completion",
|
||||
"crates/copilot",
|
||||
"crates/db",
|
||||
"crates/dev_server_projects",
|
||||
@@ -124,10 +125,6 @@ members = [
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
|
||||
#
|
||||
# Extensions
|
||||
#
|
||||
|
||||
"extensions/astro",
|
||||
"extensions/clojure",
|
||||
"extensions/csharp",
|
||||
@@ -157,25 +154,20 @@ members = [
|
||||
"extensions/vue",
|
||||
"extensions/zig",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
#
|
||||
|
||||
"tooling/xtask",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
#
|
||||
# Workspace member crates
|
||||
#
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
aho-corasick = "1.1"
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
@@ -189,6 +181,7 @@ collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
completion = { path = "crates/completion" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
db = { path = "crates/db" }
|
||||
dev_server_projects = { path = "crates/dev_server_projects" }
|
||||
@@ -209,7 +202,6 @@ go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui" }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
handlebars = "4.3"
|
||||
headless = { path = "crates/headless" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
@@ -248,7 +240,6 @@ project_symbols = { path = "crates/project_symbols" }
|
||||
proto = { path = "crates/proto" }
|
||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
remote = { path = "crates/remote" }
|
||||
remote_server = { path = "crates/remote_server" }
|
||||
@@ -294,44 +285,39 @@ worktree = { path = "crates/worktree" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
#
|
||||
# External crates
|
||||
#
|
||||
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "cacdb5bb3b72bad2c729227537979d95af75978f" }
|
||||
alacritty_terminal = "0.23"
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
ashpd = "0.9.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = "0.1"
|
||||
async-dispatcher = { version = "0.1" }
|
||||
async-fs = "1.6"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.23"
|
||||
async-tungstenite = { version = "0.16" }
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.22"
|
||||
base64 = "0.13"
|
||||
bitflags = "2.6.0"
|
||||
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"
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
blade-util = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
cap-std = "3.0"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = "0.11.6"
|
||||
clickhouse = { version = "0.11.6" }
|
||||
cocoa = "0.25"
|
||||
core-foundation = "0.9.3"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation-sys = "0.8.6"
|
||||
ctor = "0.2.6"
|
||||
dashmap = "6.0"
|
||||
dashmap = "5.5.3"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
env_logger = "0.10"
|
||||
exec = "0.3.1"
|
||||
fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
@@ -341,18 +327,16 @@ git2 = { version = "0.19", default-features = false }
|
||||
globset = "0.4"
|
||||
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
hyper = "0.14"
|
||||
html5ever = "0.27.0"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "2"
|
||||
indoc = "1"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
] }
|
||||
itertools = "0.11.0"
|
||||
jsonwebtoken = "9.3"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
@@ -374,10 +358,11 @@ prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.10.0", default-features = false }
|
||||
rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.14", default-features = false, features = [
|
||||
runtimelib = { version = "0.12", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
@@ -401,10 +386,8 @@ similar = "1.3"
|
||||
simplelog = "0.12.2"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.30.7"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.29"
|
||||
@@ -428,7 +411,7 @@ tree-sitter-css = "0.21"
|
||||
tree-sitter-elixir = "0.2"
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-go = "0.21"
|
||||
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "1f55029bacd0a6a11f6eb894c4312d429dcf735c", package = "tree-sitter-gomod" }
|
||||
tree-sitter-go-mod = { git = "https://github.com/SomeoneToIgnore/tree-sitter-go-mod", rev = "8c1f54f12bb4c846336b634bc817645d6f35d641", package = "tree-sitter-gomod"}
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work", rev = "dcbabff454703c3a4bc98a23cf8778d4be46fd22" }
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "6dd0303acf7138dd2b9b432a229e16539581c701" }
|
||||
tree-sitter-html = "0.20"
|
||||
@@ -449,32 +432,20 @@ url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
|
||||
wasmparser = "0.201"
|
||||
wasm-encoder = "0.201"
|
||||
wasmtime = { version = "21.0.1", default-features = false, features = [
|
||||
wasmtime = { version = "19.0.2", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
] }
|
||||
wasmtime-wasi = "21.0.1"
|
||||
wasmtime-wasi = "19.0.2"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
version = "0.37"
|
||||
default-features = false
|
||||
features = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
"billing",
|
||||
"checkout",
|
||||
"events",
|
||||
# The features below are only enabled to get the `events` feature to build.
|
||||
"chrono",
|
||||
"connect",
|
||||
]
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.58"
|
||||
version = "0.57"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
@@ -494,6 +465,7 @@ features = [
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_DataExchange",
|
||||
@@ -512,10 +484,6 @@ features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
# Patch Tree-sitter for updated wasmtime.
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7f4a57817d58a2f134fe863674acad6bbf007228" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
debug = "limited"
|
||||
@@ -567,6 +535,13 @@ single_range_in_vec_init = "allow"
|
||||
style = { level = "allow", priority = -1 }
|
||||
|
||||
# Individual rules that have violations in the codebase:
|
||||
almost_complete_range = "allow"
|
||||
arc_with_non_send_sync = "allow"
|
||||
borrowed_box = "allow"
|
||||
let_underscore_future = "allow"
|
||||
map_entry = "allow"
|
||||
non_canonical_partial_ord_impl = "allow"
|
||||
reversed_empty_ranges = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.80-bookworm as builder
|
||||
FROM rust:1.79-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
@@ -27,7 +27,5 @@ RUN apt-get update; \
|
||||
WORKDIR app
|
||||
COPY --from=builder /app/collab /app/collab
|
||||
COPY --from=builder /app/crates/collab/migrations /app/migrations
|
||||
COPY --from=builder /app/crates/collab/migrations_llm /app/migrations_llm
|
||||
ENV MIGRATIONS_PATH=/app/migrations
|
||||
ENV LLM_DATABASE_MIGRATIONS_PATH=/app/migrations_llm
|
||||
ENTRYPOINT ["/app/collab"]
|
||||
|
||||
2
Procfile
@@ -1,3 +1,3 @@
|
||||
collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve all
|
||||
collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve
|
||||
livekit: livekit-server --dev
|
||||
blob_store: ./script/run-local-minio
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
app: postgrest crates/collab/postgrest_app.conf
|
||||
llm: postgrest crates/collab/postgrest_llm.conf
|
||||
@@ -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="M3.43331 10.1846L6.66616 2.33334L9.89902 10.1846M3.43331 10.1846L1.9995 13.6667M3.43331 10.1846H9.89902M11.3328 13.6667L9.89902 10.1846" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.0613 13.647L9.34721 2.33334" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 459 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.8695 8.16639C14.8695 12.2258 12.0896 15.1147 7.98425 15.1147C4.04818 15.1147 0.869492 11.9361 0.869492 7.99999C0.869492 4.06393 4.04818 0.885239 7.98425 0.885239C9.90064 0.885239 11.5129 1.58811 12.7551 2.74712L10.8187 4.60901C8.28547 2.16475 3.57482 4.00081 3.57482 7.99999C3.57482 10.4816 5.5572 12.4926 7.98425 12.4926C10.8015 12.4926 11.8572 10.4729 12.0236 9.42581H7.98425V6.97868H14.7576C14.8236 7.34303 14.8695 7.69303 14.8695 8.16639Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 575 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.84221 15C2.80954 14.65 2.89261 13.8016 3.48621 13.208C2.85154 12.48 1.93501 10.6712 3.34621 9.26C2.17021 7.496 2.84221 4.808 5.30621 4.668C5.41821 4.14533 6.24388 3.06077 8.05828 3.06077C9.87268 3.06077 10.5818 4.14533 10.6938 4.668C13.1578 4.808 13.8298 7.496 12.6538 9.26C14.065 10.6712 13.1485 12.48 12.5138 13.208C13.1074 13.8016 13.1905 14.65 13.1578 15M3.93421 4.864C3.74755 3.51066 3.6549 1 4.83021 1C5.64221 0.999997 5.83821 2.68 6.14621 3.632M12.0658 4.864C12.2525 3.51066 12.3451 1 11.1698 1C10.3578 0.999997 10.1618 2.68 9.85379 3.632M8.05828 7.6965C7.48995 7.72052 6.28918 7.74527 6.28918 9.10927C6.28918 10.4733 7.54678 10.5621 8.05828 10.5621C8.56978 10.5621 9.71082 10.4733 9.71082 9.10927C9.71082 7.74527 8.62661 7.72052 8.05828 7.6965Z" stroke="black" stroke-width="1.25"/>
|
||||
<circle cx="4.98426" cy="7.76129" r="0.666553" fill="black"/>
|
||||
<circle cx="0.666553" cy="0.666553" r="0.666553" transform="matrix(-1 0 0 1 11.6823 7.09473)" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.0768 6.72994C14.3987 5.77663 14.2879 4.73232 13.7731 3.86519C12.9989 2.53519 11.4427 1.85094 9.92272 2.17294C9.24656 1.42132 8.2751 0.993879 7.25664 1C5.70301 0.996504 4.32452 1.9835 3.84655 3.44213C2.84849 3.64382 1.98699 4.26025 1.48286 5.13394C0.70294 6.46044 0.880738 8.13257 1.9227 9.27007C1.6008 10.2234 1.71164 11.2677 2.22642 12.1348C3.00057 13.4648 4.55686 14.1491 6.07679 13.8271C6.75251 14.5787 7.72441 15.0061 8.74287 14.9996C10.2974 15.0035 11.6763 14.0156 12.1543 12.5557C13.1524 12.354 14.0139 11.7376 14.518 10.8639C15.297 9.53738 15.1188 7.86657 14.0773 6.72907L14.0768 6.72994ZM8.74376 14.0848C8.12169 14.0856 7.51912 13.8708 7.0416 13.4775C7.06332 13.4661 7.10101 13.4456 7.1254 13.4307L9.95066 11.8207C10.0952 11.7398 10.1839 11.5879 10.183 11.4239V7.49382L11.377 8.17413C11.3899 8.18025 11.3983 8.1925 11.4001 8.2065V11.4611C11.3983 12.9083 10.2105 14.0817 8.74376 14.0848ZM3.03116 11.6772C2.71946 11.1461 2.60729 10.5235 2.71414 9.91932C2.73498 9.93157 2.77178 9.95388 2.79794 9.96875L5.6232 11.5788C5.76642 11.6614 5.94377 11.6614 6.08743 11.5788L9.53654 9.6135V10.9741C9.53742 10.9881 9.53077 11.0017 9.51969 11.0104L6.66383 12.6375C5.39175 13.3603 3.76719 12.9306 3.03161 11.6772H3.03116ZM2.2876 5.592C2.59797 5.06 3.08792 4.65313 3.67141 4.44182C3.67141 4.46588 3.67008 4.50832 3.67008 4.53807V7.7585C3.6692 7.92213 3.75787 8.07394 3.90198 8.15488L7.35108 10.1197L6.15704 10.8C6.14507 10.8079 6.12999 10.8092 6.11669 10.8035L3.26039 9.17513C1.99098 8.44975 1.55557 6.84719 2.28716 5.59244L2.2876 5.592ZM12.098 7.84469L8.64887 5.87944L9.84292 5.19957C9.85489 5.19169 9.86996 5.19038 9.88326 5.19607L12.7396 6.82313C14.0112 7.54807 14.447 9.15325 13.7124 10.408C13.4015 10.9391 12.912 11.346 12.329 11.5578V8.24107C12.3303 8.07744 12.2421 7.92607 12.0984 7.84469H12.098ZM13.2863 6.07982C13.2654 6.06713 13.2286 6.04525 13.2025 6.03038L10.3772 4.42038C10.234 4.33769 10.0566 4.33769 9.91297 4.42038L6.46386 6.38563V5.025C6.46298 5.011 6.46963 4.99744 6.48071 4.98869L9.33657 3.36294C10.6086 2.63888 12.235 3.06982 12.9683 4.32544C13.2783 4.85569 13.3905 5.4765 13.2854 6.07982H13.2863ZM5.81475 8.50488L4.62026 7.82457C4.6074 7.81844 4.59898 7.80619 4.59721 7.79219V4.53763C4.59809 3.08863 5.78947 1.91438 7.25797 1.91525C7.87916 1.91525 8.48039 2.1305 8.95792 2.5225C8.93619 2.53388 8.89894 2.55444 8.87412 2.56932L6.04885 4.17932C5.90431 4.26025 5.81563 4.41163 5.81652 4.57569L5.81475 8.504V8.50488ZM6.46342 7.125L7.99976 6.24957L9.53609 7.12457V8.875L7.99976 9.75L6.46342 8.875V7.125Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1882_101)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.3125 1.875C2.07088 1.875 1.875 2.07088 1.875 2.3125V11.9375H1V2.3125C1 1.58763 1.58763 1 2.3125 1H14.0344C14.6191 1 14.9118 1.70688 14.4984 2.12029L7.27887 9.33984H9.3125V8.4375H10.1875V9.55859C10.1875 9.92103 9.89369 10.2148 9.53125 10.2148H6.40387L4.89996 11.7187H11.7187V6.25H12.5937V11.7187C12.5937 12.202 12.202 12.5937 11.7187 12.5937H4.02496L2.49371 14.125H13.6875C13.9291 14.125 14.125 13.9291 14.125 13.6875V4.0625H15V13.6875C15 14.4124 14.4124 15 13.6875 15H1.96561C1.38095 15 1.08816 14.2931 1.50157 13.8797L8.69379 6.6875H6.6875V7.5625H5.8125V6.46875C5.8125 6.10631 6.10631 5.8125 6.46875 5.8125H9.56879L11.1 4.28125H4.28125V9.75H3.40625V4.28125C3.40625 3.798 3.798 3.40625 4.28125 3.40625H11.975L13.5063 1.875H2.3125Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1882_101">
|
||||
<rect width="14" height="14" fill="white" transform="translate(1 1)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1 +1,10 @@
|
||||
<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-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_430_1270)">
|
||||
<path d="M4.30957 0.857736V4.28922L2.35703 4.28788C2.10067 4.28788 1.86816 4.44057 1.76636 4.67656C1.66511 4.91229 1.71332 5.18633 1.88959 5.37304L5.53269 9.23312C5.77565 9.49028 6.22488 9.49028 6.46784 9.23312L10.1123 5.37277C10.2875 5.18794 10.3354 4.9147 10.2342 4.6763C10.1337 4.44057 9.90066 4.28788 9.66761 4.28788H7.73891L7.73891 0.857736C7.73891 0.383865 7.35504 2.35669e-07 6.88171 2.14979e-07L5.16731 1.4004e-07C4.66906 -0.000267757 4.30957 0.383865 4.30957 0.857736ZM11.1433 11.1187C11.1434 10.6687 10.7595 10.2856 10.2861 10.2856H1.71413C1.23972 10.2856 0.856659 10.6687 0.856659 11.1187C0.856659 11.6169 1.23972 12 1.71386 12H10.2861C10.7595 12 11.1433 11.6169 11.1433 11.1187Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_430_1270">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 954 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-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
|
Before Width: | Height: | Size: 358 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-file-code"><path d="M10 12.5 8 15l2 2.5"/><path d="m14 12.5 2 2.5-2 2.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/></svg>
|
||||
|
Before Width: | Height: | Size: 388 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-file-text"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
|
||||
|
Before Width: | Height: | Size: 384 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-microscope"><path d="M6 18h8"/><path d="M3 22h18"/><path d="M14 22a7 7 0 1 0 0-14h-1"/><path d="M9 14h2"/><path d="M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z"/><path d="M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"/></svg>
|
||||
|
Before Width: | Height: | Size: 418 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-route"><circle cx="6" cy="19" r="3"/><path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"/><circle cx="18" cy="5" r="3"/></svg>
|
||||
|
Before Width: | Height: | Size: 340 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4H8" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 10L11 10" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.75"/>
|
||||
<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.75"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 450 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-text-search"><path d="M21 6H3"/><path d="M10 12H3"/><path d="M10 18H3"/><circle cx="17" cy="15" r="3"/><path d="m21 19-1.9-1.9"/></svg>
|
||||
|
Before Width: | Height: | Size: 338 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-undo"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
|
||||
|
Before Width: | Height: | Size: 288 B |
@@ -2,11 +2,13 @@
|
||||
// Standard Linux bindings
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
"home": "menu::SelectFirst",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"tab": "menu::SelectNext",
|
||||
"end": "menu::SelectLast",
|
||||
"pagedown": "menu::SelectLast",
|
||||
@@ -31,20 +33,6 @@
|
||||
"f11": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker || menu",
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Prompt",
|
||||
"bindings": {
|
||||
"left": "menu::SelectPrev",
|
||||
"right": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
@@ -52,6 +40,7 @@
|
||||
"backspace": "editor::Backspace",
|
||||
"shift-backspace": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
@@ -133,7 +122,7 @@
|
||||
"bindings": {
|
||||
"alt-]": "editor::NextInlineCompletion",
|
||||
"alt-[": "editor::PreviousInlineCompletion",
|
||||
"ctrl-right": "editor::AcceptPartialInlineCompletion"
|
||||
"alt-right": "editor::AcceptPartialInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -161,9 +150,7 @@
|
||||
"bindings": {
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPrevMatch",
|
||||
"alt-m": "assistant::ToggleModelSelector",
|
||||
"ctrl-k h": "assistant::DeployHistory",
|
||||
"ctrl-k l": "assistant::DeployPromptLibrary"
|
||||
"alt-m": "assistant::ToggleModelSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -258,6 +245,13 @@
|
||||
"ctrl-alt-shift-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"]
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -275,7 +269,6 @@
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
@@ -467,16 +460,12 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
||||
// TODO: Move this to a dock open action
|
||||
"ctrl-shift-c": "collab_panel::ToggleFocus",
|
||||
"ctrl-alt-i": "zed::DebugElements",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "!Terminal",
|
||||
"bindings": {
|
||||
"ctrl-shift-c": "collab_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
@@ -491,8 +480,7 @@
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
"ctrl-shift-enter": "repl::Run"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -611,14 +599,12 @@
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"shift-ctrl-c": "terminal::Copy",
|
||||
"ctrl-insert": "terminal::Copy",
|
||||
// "ctrl-a": "editor::SelectAll", // conflicts with readline
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"shift-ctrl-v": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||
"down": ["terminal::SendKeystroke", "down"],
|
||||
|
||||
@@ -127,9 +127,7 @@
|
||||
"cmd-'": "editor::ToggleHunkDiff",
|
||||
"cmd-\"": "editor::ExpandAllHunkDiffs",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
"cmd-i": "editor::ShowSignatureHelp",
|
||||
"ctrl-f12": "editor::GoToDeclaration",
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit"
|
||||
"cmd-i": "editor::ShowSignatureHelp"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -154,7 +152,7 @@
|
||||
"bindings": {
|
||||
"alt-]": "editor::NextInlineCompletion",
|
||||
"alt-[": "editor::PreviousInlineCompletion",
|
||||
"cmd-right": "editor::AcceptPartialInlineCompletion"
|
||||
"alt-right": "editor::AcceptPartialInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -180,8 +178,7 @@
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
"ctrl-shift-enter": "repl::Run"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -189,9 +186,7 @@
|
||||
"bindings": {
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"alt-m": "assistant::ToggleModelSelector",
|
||||
"cmd-k h": "assistant::DeployHistory",
|
||||
"cmd-k l": "assistant::DeployPromptLibrary"
|
||||
"alt-m": "assistant::ToggleModelSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// For information on binding keys, see the Zed
|
||||
// documentation: https://zed.dev/docs/key-bindings
|
||||
//
|
||||
// To see the default key bindings run `zed: open default keymap`
|
||||
// To see the default key bindings run `zed: Open Default Keymap`
|
||||
// from the command palette.
|
||||
[
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"bindings": {
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
":": "command_palette::Toggle",
|
||||
"h": "vim::Left",
|
||||
"left": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
@@ -88,9 +89,8 @@
|
||||
"g t": "pane::ActivateNextItem",
|
||||
"g shift-t": "pane::ActivatePrevItem",
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToDeclaration",
|
||||
"g y": "editor::GoToTypeDefinition",
|
||||
"g shift-i": "editor::GoToImplementation",
|
||||
"g shift-d": "editor::GoToTypeDefinition",
|
||||
"g cmd-d": "editor::GoToImplementation",
|
||||
"g x": "editor::OpenUrl",
|
||||
"g n": "vim::SelectNextMatch",
|
||||
"g shift-n": "vim::SelectPreviousMatch",
|
||||
@@ -198,12 +198,17 @@
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "VimControl && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
".": "vim::Repeat",
|
||||
"c": ["vim::PushOperator", "Change"],
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
@@ -251,17 +256,9 @@
|
||||
"g c": ["vim::PushOperator", "ToggleComments"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "VimControl && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0],
|
||||
":": "vim::CountCommand"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == visual",
|
||||
"bindings": {
|
||||
":": "vim::VisualCommand",
|
||||
"u": "vim::ConvertToLowerCase",
|
||||
"U": "vim::ConvertToUpperCase",
|
||||
"o": "vim::OtherEnd",
|
||||
@@ -319,7 +316,6 @@
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||
"ctrl-r": ["vim::PushOperator", "Register"]
|
||||
}
|
||||
},
|
||||
@@ -329,7 +325,6 @@
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||
"backspace": "vim::UndoReplace",
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter"
|
||||
@@ -342,8 +337,7 @@
|
||||
"enter": "vim::Enter",
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }]
|
||||
"ctrl-[": "vim::ClearOperators"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
{{#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}}
|
||||
|
||||
{{#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}}
|
||||
|
||||
<document>
|
||||
{{{document_content}}}
|
||||
</document>
|
||||
|
||||
{{#if is_truncated}}
|
||||
The context around the relevant section has been truncated (possibly in the middle of a 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:
|
||||
|
||||
<rewrite_this>
|
||||
{{{rewrite_section}}}
|
||||
</rewrite_this>
|
||||
{{/if}}
|
||||
|
||||
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
|
||||
|
||||
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.
|
||||
|
||||
Immediately start with the following format with no remarks:
|
||||
|
||||
```
|
||||
{{REWRITTEN_CODE}}
|
||||
```
|
||||
{{/if}}
|
||||
@@ -1,87 +0,0 @@
|
||||
Guide the user through code changes in numbered steps that focus on individual functions, type definitions, etc.
|
||||
Surround each distinct step in a <step></step> XML tag.
|
||||
|
||||
Here's an example of a user-assistant dialog with step numbers and a follow-up question in Rust:
|
||||
|
||||
1. User: I want to create a User struct with name and age fields, and a method to print the user's information. Can you help me with that?
|
||||
A: Certainly! Let's create a User struct with the specified fields and a method to print the information:
|
||||
<step>Define the User struct
|
||||
```rust
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
```
|
||||
</step>
|
||||
<step>Implement methods for the User struct
|
||||
```rust
|
||||
impl User {
|
||||
fn new(name: String, age: u32) -> Self {
|
||||
User { name, age }
|
||||
}
|
||||
fn print_info(&self) {
|
||||
println!("Name: {}, Age: {}", self.name, self.age);
|
||||
}
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
This code defines a User struct and implements two methods:
|
||||
- `new`: a constructor to create a new User
|
||||
- `print_info`: a method to print the user's information
|
||||
|
||||
2. User: Great! Now, how can I create a User instance and call the print_info method?
|
||||
A: You can create a User instance and call the print_info method like this:
|
||||
|
||||
<step>Create a User instance and call print_info
|
||||
```rust
|
||||
fn main() {
|
||||
let user = User::new(String::from("Alice"), 30);
|
||||
user.print_info();
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
This will output: `Name: Alice, Age: 30`
|
||||
|
||||
3. User: Can you add an email field to the User struct and update the print_info method to include it?
|
||||
A: Certainly! Let's modify the User struct and update the print_info method:
|
||||
|
||||
<step>Add email field to User struct
|
||||
```rust
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
email: String,
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
<step>Update the new method and print_info method
|
||||
```rust
|
||||
impl User {
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
}
|
||||
fn print_info(&self) {
|
||||
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
|
||||
}
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
<step>Update the main function
|
||||
```rust
|
||||
fn main() {
|
||||
let user = User::new(String::from("Alice"), 30, String::from("alice@example.com"));
|
||||
user.print_info();
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
This will now output: `Name: Alice, Age: 30, Email: alice@example.com`
|
||||
The changes include:
|
||||
1. Adding the `email` field to the User struct
|
||||
2. Updating the `new` method to accept an email parameter
|
||||
3. Modifying the `print_info` method to include the email
|
||||
4. Updating the main function to provide an email when creating a User instance
|
||||
241
assets/prompts/operations.md
Normal file
@@ -0,0 +1,241 @@
|
||||
Your task is to map a step from the conversation above to operations on symbols inside the provided source files.
|
||||
|
||||
Guidelines:
|
||||
- There's no need to describe *what* to do, just *where* to do it.
|
||||
- If creating a file, assume any subsequent updates are included at the time of creation.
|
||||
- Don't create and then update a file.
|
||||
- We'll create it in one shot.
|
||||
- Prefer updating symbols lower in the syntax tree if possible.
|
||||
- Never include operations on a parent symbol and one of its children in the same <operations> block.
|
||||
- Never nest an operation with another operation or include CDATA or other content. All operations are leaf nodes.
|
||||
- Include a description attribute for each operation with a brief, one-line description of the change to perform.
|
||||
- Descriptions are required for all operations except delete.
|
||||
- When generating multiple operations, ensure the descriptions are specific to each individual operation.
|
||||
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
|
||||
- Don't generate multiple operations at the same location. Instead, combine them together in a single operation with a succinct combined description.
|
||||
|
||||
The available operation types are:
|
||||
|
||||
1. <update>: Modify an existing symbol in a file.
|
||||
2. <create_file>: Create a new file.
|
||||
3. <insert_sibling_after>: Add a new symbol as sibling after an existing symbol in a file.
|
||||
4. <append_child>: Add a new symbol as the last child of an existing symbol in a file.
|
||||
5. <prepend_child>: Add a new symbol as the first child of an existing symbol in a file.
|
||||
6. <delete>: Remove an existing symbol from a file. The `description` attribute is invalid for delete, but required for other ops.
|
||||
|
||||
All operations *require* a path.
|
||||
Operations that *require* a symbol: <update>, <insert_sibling_after>, <delete>
|
||||
Operations that don't allow a symbol: <create>
|
||||
Operations that have an *optional* symbol: <prepend_child>, <append_child>
|
||||
|
||||
Example 1:
|
||||
|
||||
User:
|
||||
```rs src/rectangle.rs
|
||||
struct Rectangle {
|
||||
width: f64,
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Symbols for src/rectangle.rs:
|
||||
- struct Rectangle
|
||||
- impl Rectangle
|
||||
- impl Rectangle fn new
|
||||
|
||||
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
|
||||
<step>Implement the 'Display' trait for the Rectangle struct</step>
|
||||
|
||||
What are the operations for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
|
||||
|
||||
Assistant (wrong):
|
||||
<operations>
|
||||
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_area method" />
|
||||
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_perimeter method" />
|
||||
</operations>
|
||||
|
||||
This demonstrates what NOT to do. NEVER append multiple children at the same location.
|
||||
|
||||
Assistant (corrected):
|
||||
<operations>
|
||||
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate area and perimeter methods" />
|
||||
</operations>
|
||||
|
||||
User:
|
||||
What are the operations for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<insert_sibling_after path="src/shapes.rs" symbol="impl Rectangle" description="Implement Display trait for Rectangle"/>
|
||||
</operations>
|
||||
|
||||
Example 2:
|
||||
|
||||
User:
|
||||
```rs src/user.rs
|
||||
struct User {
|
||||
pub name: String,
|
||||
age: u32,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
}
|
||||
|
||||
pub fn print_info(&self) {
|
||||
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Symbols for src/user.rs:
|
||||
- struct User
|
||||
- struct User pub name
|
||||
- struct User age
|
||||
- struct User email
|
||||
- impl User
|
||||
- impl User fn new
|
||||
- impl User pub fn print_info
|
||||
|
||||
<step>Update the 'print_info' method to use formatted output</step>
|
||||
<step>Remove the 'email' field from the User struct</step>
|
||||
|
||||
What are the operations for the step: <step>Update the 'print_info' method to use formatted output</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<update path="src/user.rs" symbol="impl User fn print_info" description="Use formatted output" />
|
||||
</operations>
|
||||
|
||||
User:
|
||||
What are the operations for the step: <step>Remove the 'email' field from the User struct</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<delete path="src/user.rs" symbol="struct User email" description="Remove the email field" />
|
||||
</operations>
|
||||
|
||||
Example 3:
|
||||
|
||||
User:
|
||||
```rs src/vehicle.rs
|
||||
struct Vehicle {
|
||||
make: String,
|
||||
model: String,
|
||||
year: u32,
|
||||
}
|
||||
|
||||
impl Vehicle {
|
||||
fn new(make: String, model: String, year: u32) -> Self {
|
||||
Vehicle { make, model, year }
|
||||
}
|
||||
|
||||
fn print_year(&self) {
|
||||
println!("Year: {}", self.year);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Symbols for src/vehicle.rs:
|
||||
- struct Vehicle
|
||||
- struct Vehicle make
|
||||
- struct Vehicle model
|
||||
- struct Vehicle year
|
||||
- impl Vehicle
|
||||
- impl Vehicle fn new
|
||||
- impl Vehicle fn print_year
|
||||
|
||||
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
|
||||
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
|
||||
|
||||
What are the operations for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<prepend_child path="src/vehicle.rs" description="Add 'use std::fmt' statement" />
|
||||
</operations>
|
||||
|
||||
User:
|
||||
What are the operations for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
|
||||
|
||||
Assistant:
|
||||
<operations>
|
||||
<insert_sibling_after path="src/vehicle.rs" symbol="impl Vehicle fn new" description="Add start_engine method"/>
|
||||
</operations>
|
||||
|
||||
Example 4:
|
||||
|
||||
User:
|
||||
```rs src/employee.rs
|
||||
struct Employee {
|
||||
name: String,
|
||||
position: String,
|
||||
salary: u32,
|
||||
department: String,
|
||||
}
|
||||
|
||||
impl Employee {
|
||||
fn new(name: String, position: String, salary: u32, department: String) -> Self {
|
||||
Employee { name, position, salary, department }
|
||||
}
|
||||
|
||||
fn print_details(&self) {
|
||||
println!("Name: {}, Position: {}, Salary: {}, Department: {}",
|
||||
self.name, self.position, self.salary, self.department);
|
||||
}
|
||||
|
||||
fn give_raise(&mut self, amount: u32) {
|
||||
self.salary += amount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Symbols for src/employee.rs:
|
||||
- struct Employee
|
||||
- struct Employee name
|
||||
- struct Employee position
|
||||
- struct Employee salary
|
||||
- struct Employee department
|
||||
- impl Employee
|
||||
- impl Employee fn new
|
||||
- impl Employee fn print_details
|
||||
- impl Employee fn give_raise
|
||||
|
||||
<step>Make salary an f32</step>
|
||||
|
||||
What are the operations for the step: <step>Make salary an f32</step>
|
||||
|
||||
A (wrong):
|
||||
<operations>
|
||||
<update path="src/employee.rs" symbol="struct Employee" description="Change the type of salary to an f32" />
|
||||
<update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
|
||||
</operations>
|
||||
|
||||
This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`.
|
||||
|
||||
A (corrected):
|
||||
<operations>
|
||||
<update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
|
||||
</operations>
|
||||
|
||||
User:
|
||||
What are the correct operations for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
|
||||
|
||||
A:
|
||||
<operations>
|
||||
<delete path="src/employee.rs" symbol="struct Employee department" />
|
||||
<update path="src/employee.rs" symbol="impl Employee fn print_details" description="Don't print the 'department' field" />
|
||||
</operations>
|
||||
|
||||
Now generate the operations for the following step.
|
||||
Output only valid XML containing valid operations with their required attributes.
|
||||
NEVER output code or any other text inside <operation> tags. If you do, you will replaced with another model.
|
||||
Your response *MUST* begin with <operations> and end with </operations>:
|
||||
@@ -1,413 +0,0 @@
|
||||
Your task is to map a step from the conversation above to suggestions on symbols inside the provided source files.
|
||||
|
||||
Guidelines:
|
||||
- There's no need to describe *what* to do, just *where* to do it.
|
||||
- If creating a file, assume any subsequent updates are included at the time of creation.
|
||||
- Don't create and then update a file.
|
||||
- We'll create it in one shot.
|
||||
- Prefer updating symbols lower in the syntax tree if possible.
|
||||
- Never include suggestions on a parent symbol and one of its children in the same suggestions block.
|
||||
- Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes.
|
||||
- Include a description attribute for each operation with a brief, one-line description of the change to perform.
|
||||
- Descriptions are required for all suggestions except delete.
|
||||
- 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.
|
||||
|
||||
Example 1:
|
||||
|
||||
User:
|
||||
```rs src/rectangle.rs
|
||||
struct Rectangle {
|
||||
width: f64,
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
|
||||
<step>Implement the 'Display' trait for the Rectangle struct</step>
|
||||
|
||||
What are the suggestions for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
|
||||
|
||||
A (wrong):
|
||||
{
|
||||
"title": "Add Rectangle methods",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "AppendChild",
|
||||
"path": "src/shapes.rs",
|
||||
"symbol": "impl Rectangle",
|
||||
"description": "Add calculate_area method"
|
||||
},
|
||||
{
|
||||
"kind": "AppendChild",
|
||||
"path": "src/shapes.rs",
|
||||
"symbol": "impl Rectangle",
|
||||
"description": "Add calculate_perimeter method"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
This demonstrates what NOT to do. NEVER append multiple children at the same location.
|
||||
|
||||
A (corrected):
|
||||
{
|
||||
"title": "Add Rectangle methods",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "AppendChild",
|
||||
"path": "src/shapes.rs",
|
||||
"symbol": "impl Rectangle",
|
||||
"description": "Add calculate area and perimeter methods"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
User:
|
||||
What are the suggestions for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Implement Display for Rectangle",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "InsertSiblingAfter",
|
||||
"path": "src/shapes.rs",
|
||||
"symbol": "impl Rectangle",
|
||||
"description": "Implement Display trait for Rectangle"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Example 2:
|
||||
|
||||
User:
|
||||
```rs src/user.rs
|
||||
struct User {
|
||||
pub name: String,
|
||||
age: u32,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
}
|
||||
|
||||
pub fn print_info(&self) {
|
||||
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<step>Update the 'print_info' method to use formatted output</step>
|
||||
<step>Remove the 'email' field from the User struct</step>
|
||||
|
||||
What are the suggestions for the step: <step>Update the 'print_info' method to use formatted output</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Use formatted output",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/user.rs",
|
||||
"symbol": "impl User pub fn print_info",
|
||||
"description": "Use formatted output"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
User:
|
||||
What are the suggestions for the step: <step>Remove the 'email' field from the User struct</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Remove email field",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Delete",
|
||||
"path": "src/user.rs",
|
||||
"symbol": "struct User email"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Example 3:
|
||||
|
||||
User:
|
||||
```rs src/vehicle.rs
|
||||
struct Vehicle {
|
||||
make: String,
|
||||
model: String,
|
||||
year: u32,
|
||||
}
|
||||
|
||||
impl Vehicle {
|
||||
fn new(make: String, model: String, year: u32) -> Self {
|
||||
Vehicle { make, model, year }
|
||||
}
|
||||
|
||||
fn print_year(&self) {
|
||||
println!("Year: {}", self.year);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
|
||||
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
|
||||
|
||||
What are the suggestions for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Add use std::fmt statement",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "PrependChild",
|
||||
"path": "src/vehicle.rs",
|
||||
"description": "Add 'use std::fmt' statement"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
User:
|
||||
What are the suggestions for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Add start_engine method",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "InsertSiblingAfter",
|
||||
"path": "src/vehicle.rs",
|
||||
"symbol": "impl Vehicle fn new",
|
||||
"description": "Add start_engine method"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Example 4:
|
||||
|
||||
User:
|
||||
```rs src/employee.rs
|
||||
struct Employee {
|
||||
name: String,
|
||||
position: String,
|
||||
salary: u32,
|
||||
department: String,
|
||||
}
|
||||
|
||||
impl Employee {
|
||||
fn new(name: String, position: String, salary: u32, department: String) -> Self {
|
||||
Employee { name, position, salary, department }
|
||||
}
|
||||
|
||||
fn print_details(&self) {
|
||||
println!("Name: {}, Position: {}, Salary: {}, Department: {}",
|
||||
self.name, self.position, self.salary, self.department);
|
||||
}
|
||||
|
||||
fn give_raise(&mut self, amount: u32) {
|
||||
self.salary += amount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<step>Make salary an f32</step>
|
||||
|
||||
What are the suggestions for the step: <step>Make salary an f32</step>
|
||||
|
||||
A (wrong):
|
||||
{
|
||||
"title": "Change salary to f32",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "struct Employee",
|
||||
"description": "Change the type of salary to an f32"
|
||||
},
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "struct Employee salary",
|
||||
"description": "Change the type to an f32"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`.
|
||||
|
||||
A (corrected):
|
||||
{
|
||||
"title": "Change salary to f32",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "struct Employee salary",
|
||||
"description": "Change the type to an f32"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
User:
|
||||
What are the correct suggestions for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Remove department",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Delete",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "struct Employee department"
|
||||
},
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "impl Employee fn print_details",
|
||||
"description": "Don't print the 'department' field"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Example 5:
|
||||
|
||||
User:
|
||||
```rs src/game.rs
|
||||
struct Player {
|
||||
name: String,
|
||||
health: i32,
|
||||
pub score: u32,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new(name: String) -> Self {
|
||||
Player { name, health: 100, score: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
struct Game {
|
||||
players: Vec<Player>,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
fn new() -> Self {
|
||||
Game { players: Vec::new() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<step>Add a 'level' field to Player and update the 'new' method</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Add level field to Player",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "InsertSiblingAfter",
|
||||
"path": "src/game.rs",
|
||||
"symbol": "struct Player pub score",
|
||||
"description": "Add level field to Player"
|
||||
},
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/game.rs",
|
||||
"symbol": "impl Player pub fn new",
|
||||
"description": "Initialize level in new method"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Example 6:
|
||||
|
||||
User:
|
||||
```rs src/config.rs
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct Config {
|
||||
settings: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn new() -> Self {
|
||||
Config { settings: HashMap::new() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<step>Add a 'load_from_file' method to Config and import necessary modules</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Add load_from_file method",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "PrependChild",
|
||||
"path": "src/config.rs",
|
||||
"description": "Import std::fs and std::io modules"
|
||||
},
|
||||
{
|
||||
"kind": "AppendChild",
|
||||
"path": "src/config.rs",
|
||||
"symbol": "impl Config",
|
||||
"description": "Add load_from_file method"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Example 7:
|
||||
|
||||
User:
|
||||
```rs src/database.rs
|
||||
pub(crate) struct Database {
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
fn new(url: &str) -> Result<Self, Error> {
|
||||
let connection = Connection::connect(url)?;
|
||||
Ok(Database { connection })
|
||||
}
|
||||
|
||||
async fn query(&self, sql: &str) -> Result<Vec<Row>, Error> {
|
||||
self.connection.query(sql, &[])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<step>Add error handling to the 'query' method and create a custom error type</step>
|
||||
|
||||
A:
|
||||
{
|
||||
"title": "Add error handling to query",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "PrependChild",
|
||||
"path": "src/database.rs",
|
||||
"description": "Import necessary error handling modules"
|
||||
},
|
||||
{
|
||||
"kind": "InsertSiblingBefore",
|
||||
"path": "src/database.rs",
|
||||
"symbol": "pub(crate) struct Database",
|
||||
"description": "Define custom DatabaseError enum"
|
||||
},
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/database.rs",
|
||||
"symbol": "impl Database async fn query",
|
||||
"description": "Implement error handling in query method"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Now generate the suggestions for the following step:
|
||||
@@ -1,18 +0,0 @@
|
||||
You are an expert terminal user.
|
||||
You will be given a description of a command and you need to respond with a command that matches the description.
|
||||
Do not include markdown blocks or any other text formatting in your response, always respond with a single command that can be executed in the given shell.
|
||||
Current OS name is '{{os}}', architecture is '{{arch}}'.
|
||||
{{#if shell}}
|
||||
Current shell is '{{shell}}'.
|
||||
{{/if}}
|
||||
{{#if working_directory}}
|
||||
Current working directory is '{{working_directory}}'.
|
||||
{{/if}}
|
||||
{{#if latest_output}}
|
||||
Latest non-empty terminal output:
|
||||
{{#each latest_output as |line|}}
|
||||
{{line}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
Here is the description of the command:
|
||||
{{{user_prompt}}}
|
||||
@@ -26,9 +26,6 @@
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Plex Mono",
|
||||
// Set the buffer text's font fallbacks, this will be merged with
|
||||
// the platform's default fallbacks.
|
||||
"buffer_font_fallbacks": [],
|
||||
// The OpenType features to enable for text in the editor.
|
||||
"buffer_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -50,11 +47,8 @@
|
||||
// },
|
||||
"buffer_line_height": "comfortable",
|
||||
// The name of a font to use for rendering text in the UI
|
||||
// You can set this to ".SystemUIFont" to use the system font
|
||||
// (On macOS) You can set this to ".SystemUIFont" to use the system font
|
||||
"ui_font_family": "Zed Plex Sans",
|
||||
// Set the UI's font fallbacks, this will be merged with the platform's
|
||||
// default font fallbacks.
|
||||
"ui_font_fallbacks": [],
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -318,7 +312,7 @@
|
||||
"auto_reveal_entries": true,
|
||||
// Whether to fold directories automatically and show compact folders
|
||||
// (e.g. "a/b/c" ) when a directory has only one subdirectory inside.
|
||||
"auto_fold_dirs": true,
|
||||
"auto_fold_dirs": false,
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
@@ -635,7 +629,7 @@
|
||||
// "option_to_meta": false,
|
||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||
// "option_to_meta": true,
|
||||
"option_as_meta": true,
|
||||
"option_as_meta": false,
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
@@ -681,10 +675,6 @@
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Plex Mono",
|
||||
// Set the terminal's font fallbacks. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font fallbacks.
|
||||
// This will be merged with the platform's default font fallbacks
|
||||
// "font_fallbacks": ["FiraCode Nerd Fonts"],
|
||||
// Sets the maximum number of lines in the terminal's scrollback buffer.
|
||||
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
@@ -865,18 +855,13 @@
|
||||
// Different settings for specific language models.
|
||||
"language_models": {
|
||||
"anthropic": {
|
||||
"version": "1",
|
||||
"api_url": "https://api.anthropic.com"
|
||||
},
|
||||
"google": {
|
||||
"api_url": "https://generativelanguage.googleapis.com"
|
||||
"openai": {
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
},
|
||||
"ollama": {
|
||||
"api_url": "http://localhost:11434"
|
||||
},
|
||||
"openai": {
|
||||
"version": "1",
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
@@ -924,8 +909,7 @@
|
||||
"vim": {
|
||||
"use_system_clipboard": "always",
|
||||
"use_multiline_find": false,
|
||||
"use_smartcase_find": false,
|
||||
"custom_digraphs": {}
|
||||
"use_smartcase_find": false
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
@@ -981,21 +965,5 @@
|
||||
// {
|
||||
// "W": "workspace::Save"
|
||||
// }
|
||||
"command_aliases": {},
|
||||
// ssh_connections is an array of ssh connections.
|
||||
// By default this setting is null, which disables the direct ssh connection support.
|
||||
// You can configure these from `project: Open Remote` in the command palette.
|
||||
// Zed's ssh support will pull configuration from your ~/.ssh too.
|
||||
// Examples:
|
||||
// [
|
||||
// {
|
||||
// "host": "example-box",
|
||||
// "projects": [
|
||||
// {
|
||||
// "paths": ["/home/user/code/zed"]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
"ssh_connections": null
|
||||
"command_aliases": {}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// documentation: https://zed.dev/docs/configuring-zed
|
||||
//
|
||||
// To see all of Zed's default settings without changing your
|
||||
// custom settings, run `zed: open default settings` from the
|
||||
// command palette
|
||||
// custom settings, run the `zed: Open Default Settings` command
|
||||
// from the command palette
|
||||
{
|
||||
"ui_font_size": 16,
|
||||
"buffer_font_size": 16,
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
mod supported_countries;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use isahc::config::Configurable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use std::{convert::TryFrom, time::Duration};
|
||||
use strum::EnumIter;
|
||||
|
||||
pub use supported_countries::*;
|
||||
|
||||
pub const ANTHROPIC_API_URL: &'static str = "https://api.anthropic.com";
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum Model {
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-20240620")]
|
||||
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3-5-sonnet-20240620")]
|
||||
Claude3_5Sonnet,
|
||||
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-20240229")]
|
||||
#[serde(alias = "claude-3-opus", rename = "claude-3-opus-20240229")]
|
||||
Claude3Opus,
|
||||
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-20240229")]
|
||||
#[serde(alias = "claude-3-sonnet", rename = "claude-3-sonnet-20240229")]
|
||||
Claude3Sonnet,
|
||||
#[serde(rename = "claude-3-haiku", alias = "claude-3-haiku-20240307")]
|
||||
#[serde(alias = "claude-3-haiku", rename = "claude-3-haiku-20240307")]
|
||||
Claude3Haiku,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
max_tokens: usize,
|
||||
/// Override this model with a different Anthropic model for tool calls.
|
||||
tool_override: Option<String>,
|
||||
},
|
||||
Custom { name: String, max_tokens: usize },
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -77,67 +68,122 @@ impl Model {
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_model_id(&self) -> &str {
|
||||
if let Self::Custom {
|
||||
tool_override: Some(tool_override),
|
||||
..
|
||||
} = self
|
||||
{
|
||||
tool_override
|
||||
} else {
|
||||
self.id()
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Role {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: String) -> Result<Self> {
|
||||
match value.as_str() {
|
||||
"user" => Ok(Self::User),
|
||||
"assistant" => Ok(Self::Assistant),
|
||||
_ => Err(anyhow!("invalid role '{value}'")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn complete(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
) -> Result<Response> {
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("Anthropic-Beta", "tools-2024-04-04")
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let serialized_request = serde_json::to_string(&request)?;
|
||||
let request = request_builder.body(AsyncBody::from(serialized_request))?;
|
||||
|
||||
let mut response = client.send(request).await?;
|
||||
if response.status().is_success() {
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
let response_message: Response = serde_json::from_slice(&body)?;
|
||||
Ok(response_message)
|
||||
} else {
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
let body_str = std::str::from_utf8(&body)?;
|
||||
Err(anyhow!(
|
||||
"Failed to connect to API: {} {}",
|
||||
response.status(),
|
||||
body_str
|
||||
))
|
||||
impl From<Role> for String {
|
||||
fn from(val: Role) -> Self {
|
||||
match val {
|
||||
Role::User => "user".to_owned(),
|
||||
Role::Assistant => "assistant".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Request {
|
||||
#[serde(serialize_with = "serialize_request_model")]
|
||||
pub model: Model,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
pub system: String,
|
||||
pub max_tokens: u32,
|
||||
}
|
||||
|
||||
fn serialize_request_model<S>(model: &Model, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&model.id())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct RequestMessage {
|
||||
pub role: Role,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ResponseEvent {
|
||||
MessageStart {
|
||||
message: ResponseMessage,
|
||||
},
|
||||
ContentBlockStart {
|
||||
index: u32,
|
||||
content_block: ContentBlock,
|
||||
},
|
||||
Ping {},
|
||||
ContentBlockDelta {
|
||||
index: u32,
|
||||
delta: TextDelta,
|
||||
},
|
||||
ContentBlockStop {
|
||||
index: u32,
|
||||
},
|
||||
MessageDelta {
|
||||
delta: ResponseMessage,
|
||||
usage: Usage,
|
||||
},
|
||||
MessageStop {},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ResponseMessage {
|
||||
#[serde(rename = "type")]
|
||||
pub message_type: Option<String>,
|
||||
pub id: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub content: Option<Vec<String>>,
|
||||
pub model: Option<String>,
|
||||
pub stop_reason: Option<String>,
|
||||
pub stop_sequence: Option<String>,
|
||||
pub usage: Option<Usage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Usage {
|
||||
pub input_tokens: Option<u32>,
|
||||
pub output_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ContentBlock {
|
||||
Text { text: String },
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum TextDelta {
|
||||
TextDelta { text: String },
|
||||
}
|
||||
|
||||
pub async fn stream_completion(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
) -> Result<BoxStream<'static, Result<Event>>> {
|
||||
let request = StreamingRequest {
|
||||
base: request,
|
||||
stream: true,
|
||||
};
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
@@ -149,9 +195,7 @@ pub async fn stream_completion(
|
||||
if let Some(low_speed_timeout) = low_speed_timeout {
|
||||
request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
|
||||
}
|
||||
let serialized_request = serde_json::to_string(&request)?;
|
||||
let request = request_builder.body(AsyncBody::from(serialized_request))?;
|
||||
|
||||
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
|
||||
let mut response = client.send(request).await?;
|
||||
if response.status().is_success() {
|
||||
let reader = BufReader::new(response.into_body());
|
||||
@@ -176,10 +220,10 @@ pub async fn stream_completion(
|
||||
|
||||
let body_str = std::str::from_utf8(&body)?;
|
||||
|
||||
match serde_json::from_str::<Event>(body_str) {
|
||||
Ok(Event::Error { error }) => Err(api_error_to_err(error)),
|
||||
match serde_json::from_str::<ResponseEvent>(body_str) {
|
||||
Ok(_) => Err(anyhow!(
|
||||
"Unexpected success response while expecting an error: '{body_str}'",
|
||||
"Unexpected success response while expecting an error: {}",
|
||||
body_str,
|
||||
)),
|
||||
Err(_) => Err(anyhow!(
|
||||
"Failed to connect to API: {} {}",
|
||||
@@ -190,193 +234,42 @@ pub async fn stream_completion(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_text_from_events(
|
||||
response: impl Stream<Item = Result<Event>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
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(api_error_to_err(error))),
|
||||
_ => None,
|
||||
},
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
}
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use http::IsahcHttpClient;
|
||||
|
||||
fn api_error_to_err(
|
||||
ApiError {
|
||||
error_type,
|
||||
message,
|
||||
}: ApiError,
|
||||
) -> anyhow::Error {
|
||||
anyhow!("API error. Type: '{error_type}', message: '{message}'",)
|
||||
}
|
||||
// #[tokio::test]
|
||||
// async fn stream_completion_success() {
|
||||
// let http_client = IsahcHttpClient::new().unwrap();
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub role: Role,
|
||||
pub content: Vec<Content>,
|
||||
}
|
||||
// let request = Request {
|
||||
// model: Model::Claude3Opus,
|
||||
// messages: vec![RequestMessage {
|
||||
// role: Role::User,
|
||||
// content: "Ping".to_string(),
|
||||
// }],
|
||||
// stream: true,
|
||||
// system: "Respond to ping with pong".to_string(),
|
||||
// max_tokens: 4096,
|
||||
// };
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
// let stream = stream_completion(
|
||||
// &http_client,
|
||||
// "https://api.anthropic.com",
|
||||
// &std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY not set"),
|
||||
// request,
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Content {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
#[serde(rename = "image")]
|
||||
Image { source: ImageSource },
|
||||
#[serde(rename = "tool_use")]
|
||||
ToolUse {
|
||||
id: String,
|
||||
name: String,
|
||||
input: serde_json::Value,
|
||||
},
|
||||
#[serde(rename = "tool_result")]
|
||||
ToolResult {
|
||||
tool_use_id: String,
|
||||
content: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ImageSource {
|
||||
#[serde(rename = "type")]
|
||||
pub source_type: String,
|
||||
pub media_type: String,
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Tool {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum ToolChoice {
|
||||
Auto,
|
||||
Any,
|
||||
Tool { name: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub model: String,
|
||||
pub max_tokens: u32,
|
||||
pub messages: Vec<Message>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tools: Vec<Tool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<ToolChoice>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub system: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<Metadata>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub stop_sequences: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub top_k: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub top_p: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StreamingRequest {
|
||||
#[serde(flatten)]
|
||||
pub base: Request,
|
||||
pub stream: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Usage {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub input_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub output_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Response {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub response_type: String,
|
||||
pub role: Role,
|
||||
pub content: Vec<Content>,
|
||||
pub model: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_reason: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_sequence: Option<String>,
|
||||
pub usage: Usage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Event {
|
||||
#[serde(rename = "message_start")]
|
||||
MessageStart { message: Response },
|
||||
#[serde(rename = "content_block_start")]
|
||||
ContentBlockStart {
|
||||
index: usize,
|
||||
content_block: Content,
|
||||
},
|
||||
#[serde(rename = "content_block_delta")]
|
||||
ContentBlockDelta { index: usize, delta: ContentDelta },
|
||||
#[serde(rename = "content_block_stop")]
|
||||
ContentBlockStop { index: usize },
|
||||
#[serde(rename = "message_delta")]
|
||||
MessageDelta { delta: MessageDelta, usage: Usage },
|
||||
#[serde(rename = "message_stop")]
|
||||
MessageStop,
|
||||
#[serde(rename = "ping")]
|
||||
Ping,
|
||||
#[serde(rename = "error")]
|
||||
Error { error: ApiError },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ContentDelta {
|
||||
#[serde(rename = "text_delta")]
|
||||
TextDelta { text: String },
|
||||
#[serde(rename = "input_json_delta")]
|
||||
InputJsonDelta { partial_json: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MessageDelta {
|
||||
pub stop_reason: Option<String>,
|
||||
pub stop_sequence: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiError {
|
||||
#[serde(rename = "type")]
|
||||
pub error_type: String,
|
||||
pub message: String,
|
||||
}
|
||||
// stream
|
||||
// .for_each(|event| async {
|
||||
// match event {
|
||||
// Ok(event) => println!("{:?}", event),
|
||||
// Err(e) => eprintln!("Error: {:?}", e),
|
||||
// }
|
||||
// })
|
||||
// .await;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Returns whether the given country code is supported by Anthropic.
|
||||
///
|
||||
/// https://www.anthropic.com/supported-countries
|
||||
pub fn is_supported_country(country_code: &str) -> bool {
|
||||
SUPPORTED_COUNTRIES.contains(&country_code)
|
||||
}
|
||||
|
||||
/// The list of country codes supported by Anthropic.
|
||||
///
|
||||
/// https://www.anthropic.com/supported-countries
|
||||
static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
vec![
|
||||
"AL", // Albania
|
||||
"DZ", // Algeria
|
||||
"AD", // Andorra
|
||||
"AO", // Angola
|
||||
"AG", // Antigua and Barbuda
|
||||
"AR", // Argentina
|
||||
"AM", // Armenia
|
||||
"AU", // Australia
|
||||
"AT", // Austria
|
||||
"AZ", // Azerbaijan
|
||||
"BS", // Bahamas
|
||||
"BH", // Bahrain
|
||||
"BD", // Bangladesh
|
||||
"BB", // Barbados
|
||||
"BE", // Belgium
|
||||
"BZ", // Belize
|
||||
"BJ", // Benin
|
||||
"BT", // Bhutan
|
||||
"BO", // Bolivia
|
||||
"BA", // Bosnia and Herzegovina
|
||||
"BW", // Botswana
|
||||
"BR", // Brazil
|
||||
"BN", // Brunei
|
||||
"BG", // Bulgaria
|
||||
"BF", // Burkina Faso
|
||||
"BI", // Burundi
|
||||
"CV", // Cabo Verde
|
||||
"KH", // Cambodia
|
||||
"CM", // Cameroon
|
||||
"CA", // Canada
|
||||
"TD", // Chad
|
||||
"CL", // Chile
|
||||
"CO", // Colombia
|
||||
"KM", // Comoros
|
||||
"CG", // Congo (Brazzaville)
|
||||
"CR", // Costa Rica
|
||||
"CI", // Côte d'Ivoire
|
||||
"HR", // Croatia
|
||||
"CY", // Cyprus
|
||||
"CZ", // Czechia (Czech Republic)
|
||||
"DK", // Denmark
|
||||
"DJ", // Djibouti
|
||||
"DM", // Dominica
|
||||
"DO", // Dominican Republic
|
||||
"EC", // Ecuador
|
||||
"EG", // Egypt
|
||||
"SV", // El Salvador
|
||||
"GQ", // Equatorial Guinea
|
||||
"EE", // Estonia
|
||||
"SZ", // Eswatini
|
||||
"FJ", // Fiji
|
||||
"FI", // Finland
|
||||
"FR", // France
|
||||
"GA", // Gabon
|
||||
"GM", // Gambia
|
||||
"GE", // Georgia
|
||||
"DE", // Germany
|
||||
"GH", // Ghana
|
||||
"GR", // Greece
|
||||
"GD", // Grenada
|
||||
"GT", // Guatemala
|
||||
"GN", // Guinea
|
||||
"GW", // Guinea-Bissau
|
||||
"GY", // Guyana
|
||||
"HT", // Haiti
|
||||
"HN", // Honduras
|
||||
"HU", // Hungary
|
||||
"IS", // Iceland
|
||||
"IN", // India
|
||||
"ID", // Indonesia
|
||||
"IQ", // Iraq
|
||||
"IE", // Ireland
|
||||
"IL", // Israel
|
||||
"IT", // Italy
|
||||
"JM", // Jamaica
|
||||
"JP", // Japan
|
||||
"JO", // Jordan
|
||||
"KZ", // Kazakhstan
|
||||
"KE", // Kenya
|
||||
"KI", // Kiribati
|
||||
"KW", // Kuwait
|
||||
"KG", // Kyrgyzstan
|
||||
"LA", // Laos
|
||||
"LV", // Latvia
|
||||
"LB", // Lebanon
|
||||
"LS", // Lesotho
|
||||
"LR", // Liberia
|
||||
"LI", // Liechtenstein
|
||||
"LT", // Lithuania
|
||||
"LU", // Luxembourg
|
||||
"MG", // Madagascar
|
||||
"MW", // Malawi
|
||||
"MY", // Malaysia
|
||||
"MV", // Maldives
|
||||
"MT", // Malta
|
||||
"MH", // Marshall Islands
|
||||
"MR", // Mauritania
|
||||
"MU", // Mauritius
|
||||
"MX", // Mexico
|
||||
"FM", // Micronesia
|
||||
"MD", // Moldova
|
||||
"MC", // Monaco
|
||||
"MN", // Mongolia
|
||||
"ME", // Montenegro
|
||||
"MA", // Morocco
|
||||
"MZ", // Mozambique
|
||||
"NA", // Namibia
|
||||
"NR", // Nauru
|
||||
"NP", // Nepal
|
||||
"NL", // Netherlands
|
||||
"NZ", // New Zealand
|
||||
"NE", // Niger
|
||||
"NG", // Nigeria
|
||||
"MK", // North Macedonia
|
||||
"NO", // Norway
|
||||
"OM", // Oman
|
||||
"PK", // Pakistan
|
||||
"PW", // Palau
|
||||
"PS", // Palestine
|
||||
"PA", // Panama
|
||||
"PG", // Papua New Guinea
|
||||
"PY", // Paraguay
|
||||
"PE", // Peru
|
||||
"PH", // Philippines
|
||||
"PL", // Poland
|
||||
"PT", // Portugal
|
||||
"QA", // Qatar
|
||||
"RO", // Romania
|
||||
"RW", // Rwanda
|
||||
"KN", // Saint Kitts and Nevis
|
||||
"LC", // Saint Lucia
|
||||
"VC", // Saint Vincent and the Grenadines
|
||||
"WS", // Samoa
|
||||
"SM", // San Marino
|
||||
"ST", // São Tomé and Príncipe
|
||||
"SA", // Saudi Arabia
|
||||
"SN", // Senegal
|
||||
"RS", // Serbia
|
||||
"SC", // Seychelles
|
||||
"SL", // Sierra Leone
|
||||
"SG", // Singapore
|
||||
"SK", // Slovakia
|
||||
"SI", // Slovenia
|
||||
"SB", // Solomon Islands
|
||||
"ZA", // South Africa
|
||||
"KR", // South Korea
|
||||
"ES", // Spain
|
||||
"LK", // Sri Lanka
|
||||
"SR", // Suriname
|
||||
"SE", // Sweden
|
||||
"CH", // Switzerland
|
||||
"TW", // Taiwan
|
||||
"TJ", // Tajikistan
|
||||
"TZ", // Tanzania
|
||||
"TH", // Thailand
|
||||
"TL", // Timor-Leste
|
||||
"TG", // Togo
|
||||
"TO", // Tonga
|
||||
"TT", // Trinidad and Tobago
|
||||
"TN", // Tunisia
|
||||
"TR", // Türkiye (Turkey)
|
||||
"TM", // Turkmenistan
|
||||
"TV", // Tuvalu
|
||||
"UG", // Uganda
|
||||
"UA", // Ukraine (except Crimea, Donetsk, and Luhansk regions)
|
||||
"AE", // United Arab Emirates
|
||||
"GB", // United Kingdom
|
||||
"US", // United States of America
|
||||
"UY", // Uruguay
|
||||
"UZ", // Uzbekistan
|
||||
"VU", // Vanuatu
|
||||
"VA", // Vatican City
|
||||
"VN", // Vietnam
|
||||
"ZM", // Zambia
|
||||
"ZW", // Zimbabwe
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
});
|
||||
@@ -11,7 +11,6 @@ use rust_embed::RustEmbed;
|
||||
#[include = "themes/**/*"]
|
||||
#[exclude = "themes/src/*"]
|
||||
#[include = "sounds/**/*"]
|
||||
#[include = "prompts/**/*"]
|
||||
#[include = "*.md"]
|
||||
#[exclude = "*.DS_Store"]
|
||||
pub struct Assets;
|
||||
|
||||
@@ -32,14 +32,13 @@ client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
completion.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
handlebars.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
@@ -48,7 +47,6 @@ indoc.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
@@ -57,7 +55,6 @@ ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
@@ -66,11 +63,11 @@ semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
@@ -78,17 +75,16 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
picker.workspace = true
|
||||
zed_actions.workspace = true
|
||||
roxmltree = "0.20.0"
|
||||
|
||||
[dev-dependencies]
|
||||
completion = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
log.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -38,7 +38,7 @@ Considering these aspects will ensure our conversation view design is optimized
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multiple people, or between multiple people and multiple bots (you and other bots).
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, 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...
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
|
||||
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod context;
|
||||
@@ -17,9 +15,9 @@ use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use completion::LanguageModelCompletionProvider;
|
||||
pub use context::*;
|
||||
pub use context_store::*;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{actions, impl_actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
@@ -28,18 +26,16 @@ use language_model::{
|
||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||
};
|
||||
pub(crate) use model_selector::*;
|
||||
pub use prompts::PromptBuilder;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, default_command, diagnostics_command, docs_command, fetch_command,
|
||||
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
|
||||
tabs_command, term_command, workflow_command,
|
||||
tabs_command, term_command,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::ResultExt;
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
@@ -50,18 +46,16 @@ actions!(
|
||||
QuoteSelection,
|
||||
InsertIntoEditor,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InsertActivePrompt,
|
||||
ShowConfiguration,
|
||||
DeployHistory,
|
||||
DeployPromptLibrary,
|
||||
ConfirmCommand,
|
||||
ToggleModelSelector,
|
||||
DebugWorkflowSteps
|
||||
DebugEditSteps
|
||||
]
|
||||
);
|
||||
|
||||
const DEFAULT_CONTEXT_LINES: usize = 20;
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct InlineAssist {
|
||||
prompt: Option<String>,
|
||||
@@ -167,7 +161,7 @@ impl Assistant {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<PromptBuilder> {
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.set_global(Assistant::default());
|
||||
AssistantSettings::register(cx);
|
||||
|
||||
@@ -198,27 +192,12 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<Pr
|
||||
|
||||
context_store::init(&client);
|
||||
prompt_library::init(cx);
|
||||
init_language_model_settings(cx);
|
||||
init_completion_provider(cx);
|
||||
assistant_slash_command::init(cx);
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some((fs.clone(), cx)))
|
||||
.log_err()
|
||||
.map(Arc::new)
|
||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||
register_slash_commands(Some(prompt_builder.clone()), cx);
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
terminal_inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
|
||||
terminal_inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
|
||||
IndexedDocsRegistry::init_global(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
@@ -236,26 +215,17 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<Pr
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
prompt_builder
|
||||
}
|
||||
|
||||
fn init_language_model_settings(cx: &mut AppContext) {
|
||||
fn init_completion_provider(cx: &mut AppContext) {
|
||||
completion::init(cx);
|
||||
update_active_language_model_from_settings(cx);
|
||||
|
||||
cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
|
||||
.detach();
|
||||
cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
|_, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
update_active_language_model_from_settings(cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
cx.observe(&LanguageModelRegistry::global(cx), |_, cx| {
|
||||
update_active_language_model_from_settings(cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -263,49 +233,37 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let provider_name = LanguageModelProviderId::from(settings.default_model.provider.clone());
|
||||
let model_id = LanguageModelId::from(settings.default_model.model.clone());
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_active_model(&provider_name, &model_id, cx);
|
||||
});
|
||||
|
||||
let Some(provider) = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.provider(&provider_name)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let models = provider.provided_models(cx);
|
||||
if let Some(model) = models.iter().find(|model| model.id() == model_id).cloned() {
|
||||
LanguageModelCompletionProvider::global(cx).update(cx, |completion_provider, cx| {
|
||||
completion_provider.set_active_model(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut AppContext) {
|
||||
fn register_slash_commands(cx: &mut AppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
||||
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
|
||||
slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
|
||||
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
||||
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
|
||||
slash_command_registry.register_command(term_command::TermSlashCommand, true);
|
||||
slash_command_registry.register_command(now_command::NowSlashCommand, true);
|
||||
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
|
||||
if let Some(prompt_builder) = prompt_builder {
|
||||
slash_command_registry.register_command(
|
||||
workflow_command::WorkflowSlashCommand::new(prompt_builder),
|
||||
true,
|
||||
);
|
||||
}
|
||||
slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
|
||||
cx.observe_flag::<docs_command::DocsSlashCommandFeatureFlag, _>({
|
||||
let slash_command_registry = slash_command_registry.clone();
|
||||
move |is_enabled, _cx| {
|
||||
if is_enabled {
|
||||
slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.observe_flag::<search_command::SearchSlashCommandFeatureFlag, _>({
|
||||
let slash_command_registry = slash_command_registry.clone();
|
||||
move |is_enabled, _cx| {
|
||||
if is_enabled {
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn humanize_token_count(count: usize) -> String {
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct AssistantSettings {
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub default_model: AssistantDefaultModel,
|
||||
pub using_outdated_settings_version: bool,
|
||||
}
|
||||
|
||||
@@ -110,15 +110,11 @@ impl AssistantSettingsContent {
|
||||
move |content, _| {
|
||||
if content.anthropic.is_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
|
||||
}
|
||||
)
|
||||
));
|
||||
Some(language_model::settings::AnthropicSettingsContent {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -149,27 +145,12 @@ impl AssistantSettingsContent {
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.openai.is_none() {
|
||||
let available_models = available_models.map(|models| {
|
||||
models
|
||||
.into_iter()
|
||||
.filter_map(|model| match model {
|
||||
open_ai::Model::Custom { name, max_tokens } => {
|
||||
Some(language_model::provider::open_ai::AvailableModel { name, max_tokens })
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
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
|
||||
}
|
||||
)
|
||||
));
|
||||
Some(language_model::settings::OpenAiSettingsContent {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -198,25 +179,25 @@ impl AssistantSettingsContent {
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
default_model.map(|model| AssistantDefaultModel {
|
||||
provider: "zed.dev".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
default_model.map(|model| AssistantDefaultModel {
|
||||
provider: "openai".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
default_model.map(|model| AssistantDefaultModel {
|
||||
provider: "anthropic".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
default_model.map(|model| AssistantDefaultModel {
|
||||
provider: "ollama".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
@@ -231,7 +212,7 @@ impl AssistantSettingsContent {
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
default_model: Some(AssistantDefaultModel {
|
||||
provider: "openai".to_string(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
@@ -268,7 +249,9 @@ impl AssistantSettingsContent {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
settings.provider = Some(AssistantProviderContentV1::ZedDotDev {
|
||||
default_model: CloudModel::from_id(&model).ok(),
|
||||
});
|
||||
}
|
||||
"anthropic" => {
|
||||
let (api_url, low_speed_timeout_in_seconds) = match &settings.provider {
|
||||
@@ -325,7 +308,7 @@ impl AssistantSettingsContent {
|
||||
_ => {}
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
settings.default_model = Some(AssistantDefaultModel { provider, model });
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
@@ -382,11 +365,11 @@ pub struct AssistantSettingsContentV2 {
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new contexts.
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
default_model: Option<AssistantDefaultModel>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelSelection {
|
||||
pub struct AssistantDefaultModel {
|
||||
#[schemars(schema_with = "providers_schema")]
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
@@ -396,18 +379,16 @@ fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema:
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"google".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
impl Default for AssistantDefaultModel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "openai".to_string(),
|
||||
@@ -440,7 +421,7 @@ pub struct AssistantSettingsContentV1 {
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the assistant service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "zed.dev"
|
||||
/// This can either be the internal `zed.dev` service or an external `openai` service,
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
}
|
||||
@@ -476,8 +457,6 @@ pub struct LegacyAssistantSettingsContent {
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
@@ -519,70 +498,103 @@ fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use gpui::{AppContext, UpdateGlobal};
|
||||
// use settings::SettingsStore;
|
||||
|
||||
use super::*;
|
||||
// use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
// #[gpui::test]
|
||||
// fn test_deserialize_assistant_settings(cx: &mut AppContext) {
|
||||
// let store = settings::SettingsStore::test(cx);
|
||||
// cx.set_global(store);
|
||||
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
// // Settings default to gpt-4-turbo.
|
||||
// AssistantSettings::register(cx);
|
||||
// assert_eq!(
|
||||
// AssistantSettings::get_global(cx).provider,
|
||||
// AssistantProvider::OpenAi {
|
||||
// model: OpenAiModel::FourOmni,
|
||||
// api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
// low_speed_timeout_in_seconds: None,
|
||||
// available_models: Default::default(),
|
||||
// }
|
||||
// );
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
model: "gpt-4o".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
// // Ensure backward-compatibility.
|
||||
// SettingsStore::update_global(cx, |store, cx| {
|
||||
// store
|
||||
// .set_user_settings(
|
||||
// r#"{
|
||||
// "assistant": {
|
||||
// "openai_api_url": "test-url",
|
||||
// }
|
||||
// }"#,
|
||||
// cx,
|
||||
// )
|
||||
// .unwrap();
|
||||
// });
|
||||
// assert_eq!(
|
||||
// AssistantSettings::get_global(cx).provider,
|
||||
// AssistantProvider::OpenAi {
|
||||
// model: OpenAiModel::FourOmni,
|
||||
// api_url: "test-url".into(),
|
||||
// low_speed_timeout_in_seconds: None,
|
||||
// available_models: Default::default(),
|
||||
// }
|
||||
// );
|
||||
// SettingsStore::update_global(cx, |store, cx| {
|
||||
// store
|
||||
// .set_user_settings(
|
||||
// r#"{
|
||||
// "assistant": {
|
||||
// "default_open_ai_model": "gpt-4-0613"
|
||||
// }
|
||||
// }"#,
|
||||
// cx,
|
||||
// )
|
||||
// .unwrap();
|
||||
// });
|
||||
// assert_eq!(
|
||||
// AssistantSettings::get_global(cx).provider,
|
||||
// AssistantProvider::OpenAi {
|
||||
// model: OpenAiModel::Four,
|
||||
// api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
// low_speed_timeout_in_seconds: None,
|
||||
// available_models: Default::default(),
|
||||
// }
|
||||
// );
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent::Versioned(
|
||||
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
}),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!assistant_settings.assistant.is_version_outdated());
|
||||
}
|
||||
}
|
||||
// // The new version supports setting a custom model when using zed.dev.
|
||||
// SettingsStore::update_global(cx, |store, cx| {
|
||||
// store
|
||||
// .set_user_settings(
|
||||
// r#"{
|
||||
// "assistant": {
|
||||
// "version": "1",
|
||||
// "provider": {
|
||||
// "name": "zed.dev",
|
||||
// "default_model": {
|
||||
// "custom": {
|
||||
// "name": "custom-provider"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }"#,
|
||||
// cx,
|
||||
// )
|
||||
// .unwrap();
|
||||
// });
|
||||
// assert_eq!(
|
||||
// AssistantSettings::get_global(cx).provider,
|
||||
// AssistantProvider::ZedDotDev {
|
||||
// model: CloudModel::Custom {
|
||||
// name: "custom-provider".into(),
|
||||
// max_tokens: None
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
prompts::PromptBuilder, Context, ContextEvent, ContextId, ContextOperation, ContextVersion,
|
||||
SavedContext, SavedContextMetadata,
|
||||
Context, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||
@@ -8,9 +8,7 @@ use clock::ReplicaId;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
use paths::contexts_dir;
|
||||
use project::Project;
|
||||
@@ -28,7 +26,6 @@ use util::{ResultExt, TryFutureExt};
|
||||
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);
|
||||
client.add_model_message_handler(ContextStore::handle_update_context);
|
||||
client.add_model_request_handler(ContextStore::handle_synchronize_contexts);
|
||||
}
|
||||
@@ -52,15 +49,8 @@ pub struct ContextStore {
|
||||
project_is_shared: bool,
|
||||
client_subscription: Option<client::Subscription>,
|
||||
_project_subscriptions: Vec<gpui::Subscription>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
pub enum ContextStoreEvent {
|
||||
ContextCreated(ContextId),
|
||||
}
|
||||
|
||||
impl EventEmitter<ContextStoreEvent> for ContextStore {}
|
||||
|
||||
enum ContextHandle {
|
||||
Weak(WeakModel<Context>),
|
||||
Strong(Model<Context>),
|
||||
@@ -83,11 +73,7 @@ impl ContextHandle {
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
pub fn new(project: Model<Project>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let languages = project.read(cx).languages().clone();
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
@@ -122,7 +108,6 @@ impl ContextStore {
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_changed(project, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
@@ -184,34 +169,6 @@ impl ContextStore {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_create_context(
|
||||
this: Model<Self>,
|
||||
_: TypedEnvelope<proto::CreateContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::CreateContextResponse> {
|
||||
let (context_id, operations) = this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_remote() {
|
||||
return Err(anyhow!("can only create contexts as the host"));
|
||||
}
|
||||
|
||||
let context = this.create(cx);
|
||||
let context_id = context.read(cx).id().clone();
|
||||
cx.emit(ContextStoreEvent::ContextCreated(context_id.clone()));
|
||||
|
||||
anyhow::Ok((
|
||||
context_id,
|
||||
context
|
||||
.read(cx)
|
||||
.serialize_ops(&ContextVersion::default(), cx),
|
||||
))
|
||||
})??;
|
||||
let operations = operations.await;
|
||||
Ok(proto::CreateContextResponse {
|
||||
context_id: context_id.to_proto(),
|
||||
context: Some(proto::Context { operations }),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_update_context(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateContext>,
|
||||
@@ -336,76 +293,12 @@ impl ContextStore {
|
||||
|
||||
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
self.languages.clone(),
|
||||
Some(self.project.clone()),
|
||||
Some(self.telemetry.clone()),
|
||||
self.prompt_builder.clone(),
|
||||
cx,
|
||||
)
|
||||
Context::local(self.languages.clone(), Some(self.telemetry.clone()), cx)
|
||||
});
|
||||
self.register_context(&context, cx);
|
||||
context
|
||||
}
|
||||
|
||||
pub fn create_remote_context(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow!("project was not remote")));
|
||||
};
|
||||
if project.is_local() {
|
||||
return Task::ready(Err(anyhow!("cannot create remote contexts as the host")));
|
||||
}
|
||||
|
||||
let replica_id = project.replica_id();
|
||||
let capability = project.capability();
|
||||
let language_registry = self.languages.clone();
|
||||
let project = self.project.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
let request = self.client.request(proto::CreateContext { project_id });
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_id = ContextId::from_proto(response.context_id);
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
replica_id,
|
||||
capability,
|
||||
language_registry,
|
||||
prompt_builder,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
let operations = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
context_proto
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(|op| ContextOperation::from_proto(op))
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.await?;
|
||||
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))??;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_local_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
@@ -417,7 +310,6 @@ impl ContextStore {
|
||||
|
||||
let fs = self.fs.clone();
|
||||
let languages = self.languages.clone();
|
||||
let project = self.project.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let load = cx.background_executor().spawn({
|
||||
let path = path.clone();
|
||||
@@ -426,20 +318,11 @@ impl ContextStore {
|
||||
SavedContext::from_json(&saved_context)
|
||||
}
|
||||
});
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let saved_context = load.await?;
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::deserialize(
|
||||
saved_context,
|
||||
path.clone(),
|
||||
languages,
|
||||
prompt_builder,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
Context::deserialize(saved_context, path.clone(), languages, Some(telemetry), cx)
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
|
||||
@@ -463,11 +346,7 @@ impl ContextStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn loaded_context_for_id(
|
||||
&self,
|
||||
id: &ContextId,
|
||||
cx: &AppContext,
|
||||
) -> Option<Model<Context>> {
|
||||
fn loaded_context_for_id(&self, id: &ContextId, cx: &AppContext) -> Option<Model<Context>> {
|
||||
self.contexts.iter().find_map(|context| {
|
||||
let context = context.upgrade()?;
|
||||
if context.read(cx).id() == id {
|
||||
@@ -498,13 +377,11 @@ impl ContextStore {
|
||||
let replica_id = project.replica_id();
|
||||
let capability = project.capability();
|
||||
let language_registry = self.languages.clone();
|
||||
let project = self.project.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let request = self.client.request(proto::OpenContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
});
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
@@ -514,8 +391,6 @@ impl ContextStore {
|
||||
replica_id,
|
||||
capability,
|
||||
language_registry,
|
||||
prompt_builder,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,44 +1,16 @@
|
||||
use feature_flags::ZedPro;
|
||||
use gpui::DismissEvent;
|
||||
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
|
||||
use proto::Plan;
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::ListItemSpacing;
|
||||
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use crate::ShowConfiguration;
|
||||
use crate::{assistant_settings::AssistantSettings, LanguageModelCompletionProvider};
|
||||
use fs::Fs;
|
||||
use gpui::Action;
|
||||
use gpui::SharedString;
|
||||
use gpui::Task;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
|
||||
const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelector<T: PopoverTrigger> {
|
||||
handle: Option<PopoverMenuHandle<Picker<ModelPickerDelegate>>>,
|
||||
handle: Option<PopoverMenuHandle<ContextMenu>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
trigger: T,
|
||||
info_text: Option<SharedString>,
|
||||
}
|
||||
|
||||
pub struct ModelPickerDelegate {
|
||||
fs: Arc<dyn Fs>,
|
||||
all_models: Vec<ModelInfo>,
|
||||
filtered_models: Vec<ModelInfo>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
provider_icon: IconName,
|
||||
availability: LanguageModelAvailability,
|
||||
is_selected: bool,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
@@ -47,253 +19,110 @@ impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
handle: None,
|
||||
fs,
|
||||
trigger,
|
||||
info_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>) -> Self {
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> 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 ModelPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_models.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix.min(self.filtered_models.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Select a model...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let all_models = self.all_models.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let filtered_models = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
if query.is_empty() {
|
||||
all_models
|
||||
} else {
|
||||
all_models
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
model_info
|
||||
.model
|
||||
.name()
|
||||
.0
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.filtered_models = filtered_models;
|
||||
this.delegate.set_selected_index(0, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(model_info) = self.filtered_models.get(self.selected_index) {
|
||||
let model = model_info.model.clone();
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings, _| {
|
||||
settings.set_model(model.clone())
|
||||
});
|
||||
|
||||
// Update the selection status
|
||||
let selected_model_id = model_info.model.id();
|
||||
let selected_provider_id = model_info.model.provider_id();
|
||||
for model in &mut self.all_models {
|
||||
model.is_selected = model.model.id() == selected_model_id
|
||||
&& model.model.provider_id() == selected_provider_id;
|
||||
}
|
||||
for model in &mut self.filtered_models {
|
||||
model.is_selected = model.model.id() == selected_model_id
|
||||
&& model.model.provider_id() == selected_provider_id;
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
let model_info = self.filtered_models.get(ix)?;
|
||||
let show_badges = cx.has_flag::<ZedPro>();
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.start_slot(
|
||||
div().pr_1().child(
|
||||
Icon::new(model_info.provider_icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.font_buffer(cx)
|
||||
.min_w(px(200.))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(model_info.model.name().0.clone()))
|
||||
.children(match model_info.availability {
|
||||
LanguageModelAvailability::Public => None,
|
||||
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
|
||||
show_badges.then(|| {
|
||||
Label::new("Pro")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(div().when(model_info.is_selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
|
||||
let plan = proto::Plan::ZedPro;
|
||||
let is_trial = false;
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.when(cx.has_flag::<ZedPro>(), |this| {
|
||||
this.child(match plan {
|
||||
// Already a zed pro subscriber
|
||||
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
|
||||
.icon(IconName::ZedAssistant)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(zed_actions::OpenAccountSettings))
|
||||
}),
|
||||
// Free user
|
||||
Plan::Free => Button::new(
|
||||
"try-pro",
|
||||
if is_trial {
|
||||
"Upgrade to Pro"
|
||||
} else {
|
||||
"Try Pro"
|
||||
},
|
||||
)
|
||||
.on_click(|_, cx| cx.open_url(TRY_ZED_PRO_URL)),
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Button::new("configure", "Configure")
|
||||
.icon(IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(ShowConfiguration.boxed_clone());
|
||||
}),
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let selected_provider = LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map(|m| m.id());
|
||||
let selected_model = LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.id());
|
||||
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
||||
let mut menu = PopoverMenu::new("model-switcher");
|
||||
if let Some(handle) = self.handle {
|
||||
menu = menu.with_handle(handle);
|
||||
}
|
||||
|
||||
let all_models = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
let provider_id = provider.id();
|
||||
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();
|
||||
|
||||
ModelInfo {
|
||||
model: model.clone(),
|
||||
provider_icon,
|
||||
availability: model.availability(),
|
||||
is_selected: selected_model.as_ref() == Some(&model.id())
|
||||
&& selected_provider.as_ref() == Some(&provider_id),
|
||||
menu.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for (index, provider) in LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.enumerate()
|
||||
{
|
||||
if index > 0 {
|
||||
menu = menu.separator();
|
||||
}
|
||||
})
|
||||
menu = menu.header(provider.name().0);
|
||||
|
||||
let available_models = provider.provided_models(cx);
|
||||
if available_models.is_empty() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
move |_| {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Settings))
|
||||
.child(Label::new("Configure"))
|
||||
.into_any()
|
||||
}
|
||||
},
|
||||
{
|
||||
let provider = provider.id();
|
||||
move |cx| {
|
||||
LanguageModelCompletionProvider::global(cx).update(
|
||||
cx,
|
||||
|completion_provider, cx| {
|
||||
completion_provider
|
||||
.set_active_provider(provider.clone(), cx)
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let selected_model = LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.id());
|
||||
let selected_provider = LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_provider()
|
||||
.map(|m| m.id());
|
||||
|
||||
for available_model in available_models {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let id = available_model.id();
|
||||
let provider_id = available_model.provider_id();
|
||||
let model_name = available_model.name().0.clone();
|
||||
let selected_model = selected_model.clone();
|
||||
let selected_provider = selected_provider.clone();
|
||||
move |_| {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(model_name.clone()))
|
||||
.when(
|
||||
selected_model.as_ref() == Some(&id)
|
||||
&& selected_provider.as_ref() == Some(&provider_id),
|
||||
|this| this.child(Icon::new(IconName::Check)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let model = available_model.clone();
|
||||
move |cx| {
|
||||
let model = model.clone();
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
menu
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let delegate = ModelPickerDelegate {
|
||||
fs: self.fs.clone(),
|
||||
all_models: all_models.clone(),
|
||||
filtered_models: all_models,
|
||||
selected_index: 0,
|
||||
};
|
||||
|
||||
let picker_view = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
|
||||
picker
|
||||
});
|
||||
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::BottomLeft)
|
||||
.into()
|
||||
})
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::BottomLeft)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::{
|
||||
slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssist, InlineAssistant,
|
||||
LanguageModelCompletionProvider,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assets::Assets;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
@@ -11,18 +13,13 @@ use futures::{
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter,
|
||||
Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
|
||||
actions, point, size, transparent_black, AppContext, AssetSource, BackgroundExecutor, Bounds,
|
||||
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
|
||||
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{
|
||||
types::{SerdeBincode, SerdeJson, Str},
|
||||
Database, RoTxn,
|
||||
};
|
||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
|
||||
use parking_lot::RwLock;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use rope::Rope;
|
||||
@@ -35,7 +32,6 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
use text::LineEnding;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
|
||||
@@ -66,11 +62,6 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(GlobalPromptStore(prompt_store_future))
|
||||
}
|
||||
|
||||
const BUILT_IN_TOOLTIP_TEXT: &'static str = concat!(
|
||||
"This prompt supports special functionality.\n",
|
||||
"It's read-only, but you can remove it from your default prompt."
|
||||
);
|
||||
|
||||
/// This function opens a new prompt library window if one doesn't exist already.
|
||||
/// If one exists, it brings it to the foreground.
|
||||
///
|
||||
@@ -238,29 +229,15 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
.end_hover_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(if prompt_id.is_built_in() {
|
||||
div()
|
||||
.id("built-in-prompt")
|
||||
.child(Icon::new(IconName::FileLock).color(Color::Muted))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Built-in prompt",
|
||||
None,
|
||||
BUILT_IN_TOOLTIP_TEXT,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
} else {
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
}))
|
||||
.into_any_element()
|
||||
})
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.selected(default)
|
||||
@@ -373,10 +350,6 @@ impl PromptLibrary {
|
||||
pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
const SAVE_THROTTLE: Duration = Duration::from_millis(500);
|
||||
|
||||
if prompt_id.is_built_in() {
|
||||
return;
|
||||
}
|
||||
|
||||
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
|
||||
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
let title = prompt_editor.title_editor.read(cx).text(cx);
|
||||
@@ -486,7 +459,6 @@ impl PromptLibrary {
|
||||
let mut editor = Editor::auto_width(cx);
|
||||
editor.set_placeholder_text("Untitled", cx);
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
|
||||
editor.set_read_only(prompt_id.is_built_in());
|
||||
editor
|
||||
});
|
||||
let body_editor = cx.new_view(|cx| {
|
||||
@@ -498,7 +470,6 @@ impl PromptLibrary {
|
||||
});
|
||||
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_read_only(prompt_id.is_built_in());
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
@@ -665,10 +636,7 @@ impl PromptLibrary {
|
||||
};
|
||||
|
||||
let prompt_editor = &self.prompt_editors[&active_prompt_id].body_editor;
|
||||
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let provider = LanguageModelCompletionProvider::read_global(cx);
|
||||
let initial_prompt = action.prompt.clone();
|
||||
if provider.is_authenticated(cx) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
@@ -757,9 +725,6 @@ impl PromptLibrary {
|
||||
}
|
||||
|
||||
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
if let Some(prompt) = self.prompt_editors.get_mut(&prompt_id) {
|
||||
let editor = &prompt.body_editor.read(cx);
|
||||
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
@@ -771,7 +736,7 @@ impl PromptLibrary {
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
model.count_tokens(
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(
|
||||
LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
@@ -784,7 +749,6 @@ impl PromptLibrary {
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
prompt_editor.token_count = Some(token_count);
|
||||
@@ -839,7 +803,7 @@ impl PromptLibrary {
|
||||
let prompt_metadata = self.store.metadata(prompt_id)?;
|
||||
let prompt_editor = &self.prompt_editors[&prompt_id];
|
||||
let focus_handle = prompt_editor.body_editor.focus_handle(cx);
|
||||
let model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let current_model = LanguageModelCompletionProvider::read_global(cx).active_model();
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
Some(
|
||||
@@ -949,7 +913,7 @@ impl PromptLibrary {
|
||||
None,
|
||||
format!(
|
||||
"Model: {}",
|
||||
model
|
||||
current_model
|
||||
.as_ref()
|
||||
.map(|model| model
|
||||
.name()
|
||||
@@ -968,23 +932,7 @@ impl PromptLibrary {
|
||||
)
|
||||
},
|
||||
))
|
||||
.child(if prompt_id.is_built_in() {
|
||||
div()
|
||||
.id("built-in-prompt")
|
||||
.child(
|
||||
Icon::new(IconName::FileLock)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Built-in prompt",
|
||||
None,
|
||||
BUILT_IN_TOOLTIP_TEXT,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
} else {
|
||||
.child(
|
||||
IconButton::new(
|
||||
"delete-prompt",
|
||||
IconName::Trash,
|
||||
@@ -1002,9 +950,8 @@ impl PromptLibrary {
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"duplicate-prompt",
|
||||
@@ -1107,30 +1054,20 @@ pub struct PromptMetadata {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum PromptId {
|
||||
User { uuid: Uuid },
|
||||
EditWorkflow,
|
||||
}
|
||||
pub struct PromptId(Uuid);
|
||||
|
||||
impl PromptId {
|
||||
pub fn new() -> PromptId {
|
||||
PromptId::User {
|
||||
uuid: Uuid::new_v4(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_built_in(&self) -> bool {
|
||||
!matches!(self, PromptId::User { .. })
|
||||
PromptId(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptStore {
|
||||
executor: BackgroundExecutor,
|
||||
env: heed::Env,
|
||||
bodies: Database<SerdeBincode<PromptId>, SerdeBincode<String>>,
|
||||
metadata: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
|
||||
metadata_cache: RwLock<MetadataCache>,
|
||||
metadata: Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
|
||||
bodies: Database<SerdeJson<PromptId>, Str>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -1141,7 +1078,7 @@ struct MetadataCache {
|
||||
|
||||
impl MetadataCache {
|
||||
fn from_db(
|
||||
db: Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
|
||||
db: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
|
||||
txn: &RoTxn,
|
||||
) -> Result<Self> {
|
||||
let mut cache = MetadataCache::default();
|
||||
@@ -1193,123 +1130,35 @@ impl PromptStore {
|
||||
let db_env = unsafe {
|
||||
heed::EnvOpenOptions::new()
|
||||
.map_size(1024 * 1024 * 1024) // 1GB
|
||||
.max_dbs(4) // Metadata and bodies (possibly v1 of both as well)
|
||||
.max_dbs(2) // bodies and metadata
|
||||
.open(db_path)?
|
||||
};
|
||||
|
||||
let mut txn = db_env.write_txn()?;
|
||||
let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
|
||||
let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
|
||||
|
||||
// Remove edit workflow prompt, as we decided to opt into it using
|
||||
// a slash command instead.
|
||||
metadata.delete(&mut txn, &PromptId::EditWorkflow).ok();
|
||||
bodies.delete(&mut txn, &PromptId::EditWorkflow).ok();
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
|
||||
|
||||
let txn = db_env.read_txn()?;
|
||||
let bodies = db_env.create_database(&mut txn, Some("bodies"))?;
|
||||
let metadata = db_env.create_database(&mut txn, Some("metadata"))?;
|
||||
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
|
||||
txn.commit()?;
|
||||
|
||||
Ok(PromptStore {
|
||||
executor,
|
||||
env: db_env,
|
||||
metadata_cache: RwLock::new(metadata_cache),
|
||||
metadata,
|
||||
bodies,
|
||||
metadata,
|
||||
metadata_cache: RwLock::new(metadata_cache),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn upgrade_dbs(
|
||||
env: &heed::Env,
|
||||
metadata_db: heed::Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
|
||||
bodies_db: heed::Database<SerdeJson<PromptId>, Str>,
|
||||
) -> Result<()> {
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||
pub struct PromptIdV1(Uuid);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PromptMetadataV1 {
|
||||
pub id: PromptIdV1,
|
||||
pub title: Option<SharedString>,
|
||||
pub default: bool,
|
||||
pub saved_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
let mut txn = env.write_txn()?;
|
||||
let Some(bodies_v1_db) = env
|
||||
.open_database::<SerdeBincode<PromptIdV1>, SerdeBincode<String>>(
|
||||
&txn,
|
||||
Some("bodies"),
|
||||
)?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut bodies_v1 = bodies_v1_db
|
||||
.iter(&txn)?
|
||||
.collect::<heed::Result<HashMap<_, _>>>()?;
|
||||
|
||||
let Some(metadata_v1_db) = env
|
||||
.open_database::<SerdeBincode<PromptIdV1>, SerdeBincode<PromptMetadataV1>>(
|
||||
&txn,
|
||||
Some("metadata"),
|
||||
)?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let metadata_v1 = metadata_v1_db
|
||||
.iter(&txn)?
|
||||
.collect::<heed::Result<HashMap<_, _>>>()?;
|
||||
|
||||
for (prompt_id_v1, metadata_v1) in metadata_v1 {
|
||||
let prompt_id_v2 = PromptId::User {
|
||||
uuid: prompt_id_v1.0,
|
||||
};
|
||||
let Some(body_v1) = bodies_v1.remove(&prompt_id_v1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if metadata_db
|
||||
.get(&txn, &prompt_id_v2)?
|
||||
.map_or(true, |metadata_v2| {
|
||||
metadata_v1.saved_at > metadata_v2.saved_at
|
||||
})
|
||||
{
|
||||
metadata_db.put(
|
||||
&mut txn,
|
||||
&prompt_id_v2,
|
||||
&PromptMetadata {
|
||||
id: prompt_id_v2,
|
||||
title: metadata_v1.title.clone(),
|
||||
default: metadata_v1.default,
|
||||
saved_at: metadata_v1.saved_at,
|
||||
},
|
||||
)?;
|
||||
bodies_db.put(&mut txn, &prompt_id_v2, &body_v1)?;
|
||||
}
|
||||
}
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(&self, id: PromptId) -> Task<Result<String>> {
|
||||
let env = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
self.executor.spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
let mut prompt = bodies
|
||||
bodies
|
||||
.get(&txn, &id)?
|
||||
.ok_or_else(|| anyhow!("prompt not found"))?
|
||||
.into();
|
||||
LineEnding::normalize(&mut prompt);
|
||||
Ok(prompt)
|
||||
.ok_or_else(|| anyhow!("prompt not found"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1398,10 +1247,6 @@ impl PromptStore {
|
||||
default: bool,
|
||||
body: Rope,
|
||||
) -> Task<Result<()>> {
|
||||
if id.is_built_in() {
|
||||
return Task::ready(Err(anyhow!("built-in prompts cannot be saved")));
|
||||
}
|
||||
|
||||
let prompt_metadata = PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
@@ -1429,26 +1274,16 @@ impl PromptStore {
|
||||
fn save_metadata(
|
||||
&self,
|
||||
id: PromptId,
|
||||
mut title: Option<SharedString>,
|
||||
title: Option<SharedString>,
|
||||
default: bool,
|
||||
) -> Task<Result<()>> {
|
||||
let mut cache = self.metadata_cache.write();
|
||||
|
||||
if id.is_built_in() {
|
||||
title = cache
|
||||
.metadata_by_id
|
||||
.get(&id)
|
||||
.and_then(|metadata| metadata.title.clone());
|
||||
}
|
||||
|
||||
let prompt_metadata = PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
};
|
||||
|
||||
cache.insert(prompt_metadata.clone());
|
||||
self.metadata_cache.write().insert(prompt_metadata.clone());
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let metadata = self.metadata;
|
||||
@@ -1465,6 +1300,17 @@ impl PromptStore {
|
||||
fn first(&self) -> Option<PromptMetadata> {
|
||||
self.metadata_cache.read().metadata.first().cloned()
|
||||
}
|
||||
|
||||
pub fn operations_prompt(&self) -> String {
|
||||
String::from_utf8(
|
||||
Assets
|
||||
.load("prompts/operations.md")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.to_vec(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a shared future to a prompt store so it can be assigned as a context global.
|
||||
|
||||
@@ -1,245 +1,135 @@
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use handlebars::{Handlebars, RenderError, TemplateError};
|
||||
use language::BufferSnapshot;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use util::ResultExt;
|
||||
use std::{fmt::Write, ops::Range};
|
||||
|
||||
#[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 fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
_project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TerminalAssistantPromptContext {
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub shell: Option<String>,
|
||||
pub working_directory: Option<String>,
|
||||
pub latest_output: Vec<String>,
|
||||
pub user_prompt: String,
|
||||
}
|
||||
|
||||
pub struct PromptBuilder {
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
}
|
||||
|
||||
impl PromptBuilder {
|
||||
pub fn new(
|
||||
fs_and_cx: Option<(Arc<dyn Fs>, &gpui::AppContext)>,
|
||||
) -> Result<Self, Box<TemplateError>> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
Self::register_templates(&mut handlebars)?;
|
||||
|
||||
let handlebars = Arc::new(Mutex::new(handlebars));
|
||||
|
||||
if let Some((fs, cx)) = fs_and_cx {
|
||||
Self::watch_fs_for_template_overrides(fs, cx, handlebars.clone());
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Here's a file of text that I'm going to ask you to make an edit to."
|
||||
)?;
|
||||
"text"
|
||||
}
|
||||
|
||||
Ok(Self { handlebars })
|
||||
}
|
||||
|
||||
fn watch_fs_for_template_overrides(
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &gpui::AppContext,
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
) {
|
||||
let templates_dir = paths::prompt_templates_dir();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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_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(())
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
) -> Result<String, RenderError> {
|
||||
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 truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.start - MAX_CTX..range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
|
||||
let after_range = range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.end..range.end + MAX_CTX
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
|
||||
let mut document_content = String::new();
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
document_content.push_str(chunk);
|
||||
Some(language_name) => {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Here's a file of {language_name} that I'm going to ask you to make an edit to."
|
||||
)?;
|
||||
"code"
|
||||
}
|
||||
if is_insert {
|
||||
document_content.push_str("<insert_here></insert_here>");
|
||||
} else {
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
};
|
||||
|
||||
const MAX_CTX: usize = 50000;
|
||||
let mut is_truncated = false;
|
||||
if range.is_empty() {
|
||||
prompt.push_str("The point you'll need to insert at is marked with <insert_here></insert_here>.\n\n<document>");
|
||||
} else {
|
||||
prompt.push_str("The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.\n\n<document>");
|
||||
}
|
||||
// Include file content.
|
||||
let before_range = 0..range.start;
|
||||
let truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.start - MAX_CTX..range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
let mut non_rewrite_len = truncated_before.len();
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
prompt.push_str("\n<rewrite_this>");
|
||||
} else {
|
||||
prompt.push_str("<insert_here></insert_here>");
|
||||
}
|
||||
let after_range = range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.end..range.end + MAX_CTX
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
non_rewrite_len += truncated_after.len();
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
write!(prompt, "</document>\n\n").unwrap();
|
||||
|
||||
if is_truncated {
|
||||
writeln!(prompt, "The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.\n")?;
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"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.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the following prompt:\n\n<prompt>\n{user_prompt}\n</prompt>",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(prompt, "Match the indentation in the original file in the inserted {content_type}, don't include any indentation on blank lines.\n").unwrap();
|
||||
prompt.push_str("Immediately start with the following format with no remarks:\n\n```\n{{INSERTED_CODE}}\n```");
|
||||
} else {
|
||||
writeln!(prompt, "Edit the section of {content_type} in <rewrite_this></rewrite_this> tags based on the following prompt:'").unwrap();
|
||||
writeln!(prompt, "\n<prompt>\n{user_prompt}\n</prompt>\n").unwrap();
|
||||
let rewrite_len = range.end - range.start;
|
||||
if rewrite_len < 20000 && rewrite_len * 2 < non_rewrite_len {
|
||||
writeln!(prompt, "And here's the section to rewrite based on that prompt again for reference:\n\n<rewrite_this>\n").unwrap();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
writeln!(prompt, "\n</rewrite_this>\n").unwrap();
|
||||
}
|
||||
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);
|
||||
}
|
||||
Some(section)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
self.handlebars.lock().render("content_prompt", &context)
|
||||
writeln!(prompt, "Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {content_type} will be preserved.\n").unwrap();
|
||||
write!(
|
||||
prompt,
|
||||
"Start at the indentation level in the original file in the rewritten {content_type}. "
|
||||
)
|
||||
.unwrap();
|
||||
prompt.push_str("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.push_str("\n\nImmediately start with the following format with no remarks:\n\n```\n{{REWRITTEN_CODE}}\n```");
|
||||
}
|
||||
|
||||
pub fn generate_terminal_assistant_prompt(
|
||||
&self,
|
||||
user_prompt: &str,
|
||||
shell: Option<&str>,
|
||||
working_directory: Option<&str>,
|
||||
latest_output: &[String],
|
||||
) -> Result<String, RenderError> {
|
||||
let context = TerminalAssistantPromptContext {
|
||||
os: std::env::consts::OS.to_string(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
shell: shell.map(|s| s.to_string()),
|
||||
working_directory: working_directory.map(|s| s.to_string()),
|
||||
latest_output: latest_output.to_vec(),
|
||||
user_prompt: user_prompt.to_string(),
|
||||
};
|
||||
|
||||
self.handlebars
|
||||
.lock()
|
||||
.render("terminal_assistant_prompt", &context)
|
||||
}
|
||||
|
||||
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
|
||||
self.handlebars.lock().render("edit_workflow", &())
|
||||
}
|
||||
|
||||
pub fn generate_step_resolution_prompt(&self) -> Result<String, RenderError> {
|
||||
self.handlebars.lock().render("step_resolution", &())
|
||||
}
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
pub fn generate_terminal_assistant_prompt(
|
||||
user_prompt: &str,
|
||||
shell: Option<&str>,
|
||||
working_directory: Option<&str>,
|
||||
) -> String {
|
||||
let mut prompt = String::new();
|
||||
writeln!(&mut prompt, "You are an expert terminal user.").unwrap();
|
||||
writeln!(&mut prompt, "You will be given a description of a command and you need to respond with a command that matches the description.").unwrap();
|
||||
writeln!(&mut prompt, "Do not include markdown blocks or any other text formatting in your response, always respond with a single command that can be executed in the given shell.").unwrap();
|
||||
if let Some(shell) = shell {
|
||||
writeln!(&mut prompt, "Current shell is '{shell}'.").unwrap();
|
||||
}
|
||||
if let Some(working_directory) = working_directory {
|
||||
writeln!(
|
||||
&mut prompt,
|
||||
"Current working directory is '{working_directory}'."
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(&mut prompt, "Here is the description of the command:").unwrap();
|
||||
prompt.push_str(user_prompt);
|
||||
prompt
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ pub mod search_command;
|
||||
pub mod symbols_command;
|
||||
pub mod tabs_command;
|
||||
pub mod term_command;
|
||||
pub mod workflow_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
|
||||
@@ -46,7 +46,7 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
|
||||
@@ -44,7 +44,7 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let store = PromptStore::global(cx);
|
||||
|
||||
@@ -33,7 +33,7 @@ impl DiagnosticsSlashCommand {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
let path_prefix: Arc<str> = "".into();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
@@ -158,7 +158,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
|
||||
@@ -7,7 +7,6 @@ use anyhow::{anyhow, bail, Result};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use feature_flags::FeatureFlag;
|
||||
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView};
|
||||
use indexed_docs::{
|
||||
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
|
||||
@@ -19,12 +18,6 @@ use ui::prelude::*;
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct DocsSlashCommandFeatureFlag;
|
||||
|
||||
impl FeatureFlag for DocsSlashCommandFeatureFlag {
|
||||
const NAME: &'static str = "docs-slash-command";
|
||||
}
|
||||
|
||||
pub(crate) struct DocsSlashCommand;
|
||||
|
||||
impl DocsSlashCommand {
|
||||
@@ -226,7 +219,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
if index {
|
||||
// We don't need to hold onto this task, as the `IndexedDocsStore` will hold it
|
||||
// until it completes.
|
||||
drop(store.clone().index(package.as_str().into()));
|
||||
let _ = store.clone().index(package.as_str().into());
|
||||
}
|
||||
|
||||
let items = store.search(package).await;
|
||||
@@ -249,7 +242,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(argument) = argument else {
|
||||
|
||||
@@ -129,7 +129,7 @@ impl SlashCommand for FetchSlashCommand {
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(argument) = argument else {
|
||||
@@ -150,10 +150,6 @@ impl SlashCommand for FetchSlashCommand {
|
||||
let url = SharedString::from(url);
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
if text.trim().is_empty() {
|
||||
bail!("no textual content found");
|
||||
}
|
||||
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
|
||||
@@ -29,7 +29,7 @@ impl FileSlashCommand {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
let path_prefix: Arc<str> = "".into();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
@@ -136,7 +136,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
@@ -273,25 +273,20 @@ fn collect_files(
|
||||
continue;
|
||||
};
|
||||
if let Some(buffer) = open_buffer_task.await.log_err() {
|
||||
let buffer_snapshot =
|
||||
cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
|
||||
let snapshot = cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
|
||||
let prev_len = text.len();
|
||||
collect_file_content(
|
||||
&mut text,
|
||||
&buffer_snapshot,
|
||||
path_including_worktree_name.to_string_lossy().to_string(),
|
||||
);
|
||||
collect_file_content(&mut text, &snapshot, filename.clone());
|
||||
text.push('\n');
|
||||
if !write_single_file_diagnostics(
|
||||
&mut text,
|
||||
Some(&path_including_worktree_name),
|
||||
&buffer_snapshot,
|
||||
&snapshot,
|
||||
) {
|
||||
text.pop();
|
||||
}
|
||||
ranges.push((
|
||||
prev_len..text.len(),
|
||||
path_including_worktree_name,
|
||||
PathBuf::from(filename),
|
||||
EntryType::File,
|
||||
));
|
||||
text.push('\n');
|
||||
|
||||
@@ -44,7 +44,7 @@ impl SlashCommand for NowSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let now = Local::now();
|
||||
|
||||
@@ -119,7 +119,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
|
||||
@@ -55,7 +55,7 @@ impl SlashCommand for PromptSlashCommand {
|
||||
self: Arc<Self>,
|
||||
title: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(title) = title else {
|
||||
|
||||
@@ -5,7 +5,6 @@ use super::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use feature_flags::FeatureFlag;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{CodeLabel, LineEnding, LspAdapterDelegate};
|
||||
use semantic_index::SemanticIndex;
|
||||
@@ -18,12 +17,6 @@ use ui::{prelude::*, IconName};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct SearchSlashCommandFeatureFlag;
|
||||
|
||||
impl FeatureFlag for SearchSlashCommandFeatureFlag {
|
||||
const NAME: &'static str = "search-slash-command";
|
||||
}
|
||||
|
||||
pub(crate) struct SearchSlashCommand;
|
||||
|
||||
impl SlashCommand for SearchSlashCommand {
|
||||
@@ -61,7 +54,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
|
||||
@@ -42,7 +42,7 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
|
||||
@@ -46,7 +46,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let open_buffers = workspace.update(cx, |workspace, cx| {
|
||||
|
||||
@@ -9,9 +9,7 @@ use gpui::{AppContext, Task, WeakView};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use ui::prelude::*;
|
||||
use workspace::{dock::Panel, Workspace};
|
||||
|
||||
use crate::DEFAULT_CONTEXT_LINES;
|
||||
use workspace::Workspace;
|
||||
|
||||
use super::create_label_for_command;
|
||||
|
||||
@@ -58,7 +56,7 @@ impl SlashCommand for TermSlashCommand {
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
@@ -67,17 +65,17 @@ impl SlashCommand for TermSlashCommand {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no terminal panel open")));
|
||||
};
|
||||
let Some(active_terminal) = terminal_panel.read(cx).pane().and_then(|pane| {
|
||||
pane.read(cx)
|
||||
.active_item()
|
||||
.and_then(|t| t.downcast::<TerminalView>())
|
||||
}) else {
|
||||
let Some(active_terminal) = terminal_panel
|
||||
.read(cx)
|
||||
.pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|t| t.downcast::<TerminalView>())
|
||||
else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
|
||||
};
|
||||
|
||||
let line_count = argument
|
||||
.and_then(|a| parse_argument(a))
|
||||
.unwrap_or(DEFAULT_CONTEXT_LINES);
|
||||
let line_count = argument.and_then(|a| parse_argument(a)).unwrap_or(20);
|
||||
|
||||
let lines = active_terminal
|
||||
.read(cx)
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
use crate::prompts::PromptBuilder;
|
||||
use std::sync::Arc;
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::prelude::*;
|
||||
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct WorkflowSlashCommand {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
impl WorkflowSlashCommand {
|
||||
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
|
||||
Self { prompt_builder }
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for WorkflowSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"workflow".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert a prompt that opts into the edit workflow".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Workflow Prompt".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
cx.spawn(|_cx| async move {
|
||||
let text = prompt_builder.generate_workflow_prompt()?;
|
||||
let range = 0..text.len();
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::Route,
|
||||
label: "Workflow".into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use collections::HashMap;
|
||||
use ordered_float::OrderedFloat;
|
||||
use rope::{Point, Rope, TextSummary};
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::{self, Debug},
|
||||
@@ -65,11 +64,11 @@ impl Debug for Matrix {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CharOperation {
|
||||
#[derive(Debug)]
|
||||
pub enum Hunk {
|
||||
Insert { text: String },
|
||||
Delete { bytes: usize },
|
||||
Keep { bytes: usize },
|
||||
Remove { len: usize },
|
||||
Keep { len: usize },
|
||||
}
|
||||
|
||||
pub struct StreamingDiff {
|
||||
@@ -104,7 +103,7 @@ impl StreamingDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_new(&mut self, text: &str) -> Vec<CharOperation> {
|
||||
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
|
||||
self.new.extend(text.chars());
|
||||
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
|
||||
|
||||
@@ -146,7 +145,7 @@ impl StreamingDiff {
|
||||
hunks
|
||||
}
|
||||
|
||||
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<CharOperation> {
|
||||
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
|
||||
let mut pending_insert: Option<Range<usize>> = None;
|
||||
let mut hunks = Vec::new();
|
||||
let mut i = old_text_ix;
|
||||
@@ -186,22 +185,22 @@ impl StreamingDiff {
|
||||
}
|
||||
} else {
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(CharOperation::Insert {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let char_len = self.old[i - 1].len_utf8();
|
||||
if prev_i == i - 1 && prev_j == j {
|
||||
if let Some(CharOperation::Delete { bytes: len }) = hunks.last_mut() {
|
||||
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(CharOperation::Delete { bytes: char_len })
|
||||
hunks.push(Hunk::Remove { len: char_len })
|
||||
}
|
||||
} else if let Some(CharOperation::Keep { bytes: len }) = hunks.last_mut() {
|
||||
} else if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(CharOperation::Keep { bytes: char_len })
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +209,7 @@ impl StreamingDiff {
|
||||
}
|
||||
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(CharOperation::Insert {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
@@ -219,853 +218,74 @@ impl StreamingDiff {
|
||||
hunks
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Vec<CharOperation> {
|
||||
pub fn finish(self) -> Vec<Hunk> {
|
||||
self.backtrack(self.old.len(), self.new.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LineOperation {
|
||||
Insert { lines: u32 },
|
||||
Delete { lines: u32 },
|
||||
Keep { lines: u32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LineDiff {
|
||||
inserted_newline_at_end: bool,
|
||||
/// The extent of kept and deleted text.
|
||||
old_end: Point,
|
||||
/// The extent of kept and inserted text.
|
||||
new_end: Point,
|
||||
/// Deleted rows, expressed in terms of the old text.
|
||||
deleted_rows: BTreeSet<u32>,
|
||||
/// Inserted rows, expressed in terms of the new text.
|
||||
inserted_rows: BTreeSet<u32>,
|
||||
buffered_insert: String,
|
||||
/// After deleting a newline, we buffer deletion until we keep or insert a character.
|
||||
buffered_delete: usize,
|
||||
}
|
||||
|
||||
impl LineDiff {
|
||||
pub fn push_char_operations<'a>(
|
||||
&mut self,
|
||||
operations: impl IntoIterator<Item = &'a CharOperation>,
|
||||
old_text: &Rope,
|
||||
) {
|
||||
for operation in operations {
|
||||
self.push_char_operation(operation, old_text);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_char_operation(&mut self, operation: &CharOperation, old_text: &Rope) {
|
||||
match operation {
|
||||
CharOperation::Insert { text } => {
|
||||
self.flush_delete(old_text);
|
||||
|
||||
if is_line_start(self.old_end) {
|
||||
if let Some(newline_ix) = text.rfind('\n') {
|
||||
let (prefix, suffix) = text.split_at(newline_ix + 1);
|
||||
self.buffered_insert.push_str(prefix);
|
||||
self.flush_insert(old_text);
|
||||
self.buffered_insert.push_str(suffix);
|
||||
} else {
|
||||
self.buffered_insert.push_str(&text);
|
||||
}
|
||||
} else {
|
||||
self.buffered_insert.push_str(&text);
|
||||
if !text.ends_with('\n') {
|
||||
self.flush_insert(old_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
CharOperation::Delete { bytes } => {
|
||||
self.buffered_delete += bytes;
|
||||
|
||||
let common_suffix_len = self.trim_buffered_end(old_text);
|
||||
self.flush_insert(old_text);
|
||||
|
||||
if common_suffix_len > 0 || !is_line_end(self.old_end, old_text) {
|
||||
self.flush_delete(old_text);
|
||||
self.keep(common_suffix_len, old_text);
|
||||
}
|
||||
}
|
||||
CharOperation::Keep { bytes } => {
|
||||
self.flush_delete(old_text);
|
||||
self.flush_insert(old_text);
|
||||
self.keep(*bytes, old_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_insert(&mut self, old_text: &Rope) {
|
||||
if self.buffered_insert.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_start = self.new_end;
|
||||
let lines = TextSummary::from(self.buffered_insert.as_str()).lines;
|
||||
self.new_end += lines;
|
||||
|
||||
if is_line_start(self.old_end) {
|
||||
if self.new_end.column == 0 {
|
||||
self.inserted_rows.extend(new_start.row..self.new_end.row);
|
||||
} else {
|
||||
self.deleted_rows.insert(self.old_end.row);
|
||||
self.inserted_rows.extend(new_start.row..=self.new_end.row);
|
||||
}
|
||||
} else if is_line_end(self.old_end, old_text) {
|
||||
if self.buffered_insert.starts_with('\n') {
|
||||
self.inserted_rows
|
||||
.extend(new_start.row + 1..=self.new_end.row);
|
||||
self.inserted_newline_at_end = true;
|
||||
} else {
|
||||
if !self.inserted_newline_at_end {
|
||||
self.deleted_rows.insert(self.old_end.row);
|
||||
}
|
||||
self.inserted_rows.extend(new_start.row..=self.new_end.row);
|
||||
}
|
||||
} else {
|
||||
self.deleted_rows.insert(self.old_end.row);
|
||||
self.inserted_rows.extend(new_start.row..=self.new_end.row);
|
||||
}
|
||||
|
||||
self.buffered_insert.clear();
|
||||
}
|
||||
|
||||
fn flush_delete(&mut self, old_text: &Rope) {
|
||||
if self.buffered_delete == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let old_start = self.old_end;
|
||||
self.old_end =
|
||||
old_text.offset_to_point(old_text.point_to_offset(self.old_end) + self.buffered_delete);
|
||||
|
||||
if is_line_end(old_start, old_text) && is_line_end(self.old_end, old_text) {
|
||||
self.deleted_rows
|
||||
.extend(old_start.row + 1..=self.old_end.row);
|
||||
} else if is_line_start(old_start)
|
||||
&& (is_line_start(self.old_end) && self.old_end < old_text.max_point())
|
||||
&& self.new_end.column == 0
|
||||
{
|
||||
self.deleted_rows.extend(old_start.row..self.old_end.row);
|
||||
} else {
|
||||
self.inserted_rows.insert(self.new_end.row);
|
||||
self.deleted_rows.extend(old_start.row..=self.old_end.row);
|
||||
}
|
||||
|
||||
self.inserted_newline_at_end = false;
|
||||
self.buffered_delete = 0;
|
||||
}
|
||||
|
||||
fn keep(&mut self, bytes: usize, old_text: &Rope) {
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let lines =
|
||||
old_text.offset_to_point(old_text.point_to_offset(self.old_end) + bytes) - self.old_end;
|
||||
self.old_end += lines;
|
||||
self.new_end += lines;
|
||||
self.inserted_newline_at_end = false;
|
||||
}
|
||||
|
||||
fn trim_buffered_end(&mut self, old_text: &Rope) -> usize {
|
||||
let old_start_offset = old_text.point_to_offset(self.old_end);
|
||||
let old_end_offset = old_start_offset + self.buffered_delete;
|
||||
|
||||
let new_chars = self.buffered_insert.chars().rev();
|
||||
let old_chars = old_text
|
||||
.chunks_in_range(old_start_offset..old_end_offset)
|
||||
.flat_map(|chunk| chunk.chars().rev());
|
||||
|
||||
let mut common_suffix_len = 0;
|
||||
for (new_ch, old_ch) in new_chars.zip(old_chars) {
|
||||
if new_ch == old_ch {
|
||||
common_suffix_len += new_ch.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.buffered_delete -= common_suffix_len;
|
||||
self.buffered_insert
|
||||
.truncate(self.buffered_insert.len() - common_suffix_len);
|
||||
|
||||
common_suffix_len
|
||||
}
|
||||
|
||||
pub fn finish(&mut self, old_text: &Rope) {
|
||||
self.flush_insert(old_text);
|
||||
self.flush_delete(old_text);
|
||||
|
||||
let old_start = self.old_end;
|
||||
self.old_end = old_text.max_point();
|
||||
self.new_end += self.old_end - old_start;
|
||||
}
|
||||
|
||||
pub fn line_operations(&self) -> Vec<LineOperation> {
|
||||
let mut ops = Vec::new();
|
||||
let mut deleted_rows = self.deleted_rows.iter().copied().peekable();
|
||||
let mut inserted_rows = self.inserted_rows.iter().copied().peekable();
|
||||
let mut old_row = 0;
|
||||
let mut new_row = 0;
|
||||
|
||||
while deleted_rows.peek().is_some() || inserted_rows.peek().is_some() {
|
||||
// Check for a run of deleted lines at current old row.
|
||||
if Some(old_row) == deleted_rows.peek().copied() {
|
||||
if let Some(LineOperation::Delete { lines }) = ops.last_mut() {
|
||||
*lines += 1;
|
||||
} else {
|
||||
ops.push(LineOperation::Delete { lines: 1 });
|
||||
}
|
||||
old_row += 1;
|
||||
deleted_rows.next();
|
||||
} else if Some(new_row) == inserted_rows.peek().copied() {
|
||||
if let Some(LineOperation::Insert { lines }) = ops.last_mut() {
|
||||
*lines += 1;
|
||||
} else {
|
||||
ops.push(LineOperation::Insert { lines: 1 });
|
||||
}
|
||||
new_row += 1;
|
||||
inserted_rows.next();
|
||||
} else {
|
||||
// Keep lines until the next deletion, insertion, or the end of the old text.
|
||||
let lines_to_next_deletion = inserted_rows
|
||||
.peek()
|
||||
.copied()
|
||||
.unwrap_or(self.new_end.row + 1)
|
||||
- new_row;
|
||||
let lines_to_next_insertion =
|
||||
deleted_rows.peek().copied().unwrap_or(self.old_end.row + 1) - old_row;
|
||||
let kept_lines =
|
||||
cmp::max(1, cmp::min(lines_to_next_insertion, lines_to_next_deletion));
|
||||
if kept_lines > 0 {
|
||||
ops.push(LineOperation::Keep { lines: kept_lines });
|
||||
old_row += kept_lines;
|
||||
new_row += kept_lines;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if old_row < self.old_end.row + 1 {
|
||||
ops.push(LineOperation::Keep {
|
||||
lines: self.old_end.row + 1 - old_row,
|
||||
});
|
||||
}
|
||||
|
||||
ops
|
||||
}
|
||||
}
|
||||
|
||||
fn is_line_start(point: Point) -> bool {
|
||||
point.column == 0
|
||||
}
|
||||
|
||||
fn is_line_end(point: Point, text: &Rope) -> bool {
|
||||
text.line_len(point.row) == point.column
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_delete_first_of_two_lines() {
|
||||
let old_text = "aaaa\nbbbb";
|
||||
let char_ops = vec![
|
||||
CharOperation::Delete { bytes: 5 },
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
];
|
||||
let expected_line_ops = vec![
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Keep { lines: 1 },
|
||||
];
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &expected_line_ops)
|
||||
);
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(line_ops, expected_line_ops);
|
||||
}
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_diffs(mut rng: StdRng) {
|
||||
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
let new_text_len = env::var("NEW_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
#[test]
|
||||
fn test_delete_second_of_two_lines() {
|
||||
let old_text = "aaaa\nbbbb";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 5 },
|
||||
CharOperation::Delete { bytes: 4 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Keep { lines: 1 },
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Insert { lines: 1 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
let old = util::RandomCharIter::new(&mut rng)
|
||||
.take(old_text_len)
|
||||
.collect::<String>();
|
||||
log::info!("old text: {:?}", old);
|
||||
|
||||
#[test]
|
||||
fn test_add_new_line() {
|
||||
let old_text = "aaaa\nbbbb";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 9 },
|
||||
CharOperation::Insert {
|
||||
text: "\ncccc".into(),
|
||||
},
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Keep { lines: 2 },
|
||||
LineOperation::Insert { lines: 1 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_line_in_middle() {
|
||||
let old_text = "aaaa\nbbbb\ncccc";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 5 },
|
||||
CharOperation::Delete { bytes: 5 },
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Keep { lines: 1 },
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Keep { lines: 1 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_line() {
|
||||
let old_text = "aaaa\nbbbb\ncccc";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 5 },
|
||||
CharOperation::Delete { bytes: 4 },
|
||||
CharOperation::Insert {
|
||||
text: "BBBB".into(),
|
||||
},
|
||||
CharOperation::Keep { bytes: 5 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Keep { lines: 1 },
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Insert { lines: 1 },
|
||||
LineOperation::Keep { lines: 1 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_edits_on_different_lines() {
|
||||
let old_text = "aaaa\nbbbb\ncccc\ndddd";
|
||||
let char_ops = vec![
|
||||
CharOperation::Insert { text: "A".into() },
|
||||
CharOperation::Keep { bytes: 9 },
|
||||
CharOperation::Delete { bytes: 5 },
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
CharOperation::Insert {
|
||||
text: "\nEEEE".into(),
|
||||
},
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Insert { lines: 1 },
|
||||
LineOperation::Keep { lines: 1 },
|
||||
LineOperation::Delete { lines: 2 },
|
||||
LineOperation::Insert { lines: 2 },
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_at_end_of_line() {
|
||||
let old_text = "aaaa\nbbbb\ncccc";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
CharOperation::Insert { text: "A".into() },
|
||||
CharOperation::Keep { bytes: 10 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Insert { lines: 1 },
|
||||
LineOperation::Keep { lines: 2 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_newline_character() {
|
||||
let old_text = "aaaabbbb";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
CharOperation::Insert { text: "\n".into() },
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
];
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Insert { lines: 2 }
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_newline_at_beginning() {
|
||||
let old_text = "aaaa\nbbbb";
|
||||
let char_ops = vec![
|
||||
CharOperation::Insert { text: "\n".into() },
|
||||
CharOperation::Keep { bytes: 9 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Insert { lines: 1 },
|
||||
LineOperation::Keep { lines: 2 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_newline() {
|
||||
let old_text = "aaaa\nbbbb";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
CharOperation::Delete { bytes: 1 },
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Delete { lines: 2 },
|
||||
LineOperation::Insert { lines: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_multiple_newlines() {
|
||||
let old_text = "aaaa\nbbbb";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 5 },
|
||||
CharOperation::Insert {
|
||||
text: "\n\n".into(),
|
||||
},
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Keep { lines: 1 },
|
||||
LineOperation::Insert { lines: 2 },
|
||||
LineOperation::Keep { lines: 1 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_multiple_newlines() {
|
||||
let old_text = "aaaa\n\n\nbbbb";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 5 },
|
||||
CharOperation::Delete { bytes: 2 },
|
||||
CharOperation::Keep { bytes: 4 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Keep { lines: 1 },
|
||||
LineOperation::Delete { lines: 2 },
|
||||
LineOperation::Keep { lines: 1 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_scenario() {
|
||||
let old_text = "line1\nline2\nline3\nline4";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 6 },
|
||||
CharOperation::Insert {
|
||||
text: "inserted\n".into(),
|
||||
},
|
||||
CharOperation::Delete { bytes: 6 },
|
||||
CharOperation::Keep { bytes: 5 },
|
||||
CharOperation::Insert {
|
||||
text: "\nnewline".into(),
|
||||
},
|
||||
CharOperation::Keep { bytes: 6 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Keep { lines: 1 },
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Insert { lines: 1 },
|
||||
LineOperation::Keep { lines: 1 },
|
||||
LineOperation::Insert { lines: 1 },
|
||||
LineOperation::Keep { lines: 1 }
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(new_text, "line1\ninserted\nline3\nnewline\nline4");
|
||||
assert_eq!(
|
||||
apply_line_operations(old_text, &new_text, &line_ops),
|
||||
new_text,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleaning_up_common_suffix() {
|
||||
let old_text = concat!(
|
||||
" for y in 0..size.y() {\n",
|
||||
" let a = 10;\n",
|
||||
" let b = 20;\n",
|
||||
" }",
|
||||
);
|
||||
let char_ops = [
|
||||
CharOperation::Keep { bytes: 8 },
|
||||
CharOperation::Insert { text: "let".into() },
|
||||
CharOperation::Insert {
|
||||
text: " mut".into(),
|
||||
},
|
||||
CharOperation::Insert { text: " y".into() },
|
||||
CharOperation::Insert { text: " =".into() },
|
||||
CharOperation::Insert { text: " 0".into() },
|
||||
CharOperation::Insert { text: ";".into() },
|
||||
CharOperation::Insert { text: "\n".into() },
|
||||
CharOperation::Insert {
|
||||
text: " while".into(),
|
||||
},
|
||||
CharOperation::Insert { text: " y".into() },
|
||||
CharOperation::Insert {
|
||||
text: " < size".into(),
|
||||
},
|
||||
CharOperation::Insert { text: ".".into() },
|
||||
CharOperation::Insert { text: "y".into() },
|
||||
CharOperation::Insert { text: "()".into() },
|
||||
CharOperation::Insert { text: " {".into() },
|
||||
CharOperation::Insert { text: "\n".into() },
|
||||
CharOperation::Delete { bytes: 23 },
|
||||
CharOperation::Keep { bytes: 23 },
|
||||
CharOperation::Keep { bytes: 1 },
|
||||
CharOperation::Keep { bytes: 23 },
|
||||
CharOperation::Keep { bytes: 1 },
|
||||
CharOperation::Keep { bytes: 8 },
|
||||
CharOperation::Insert {
|
||||
text: " y".into(),
|
||||
},
|
||||
CharOperation::Insert { text: " +=".into() },
|
||||
CharOperation::Insert { text: " 1".into() },
|
||||
CharOperation::Insert { text: ";".into() },
|
||||
CharOperation::Insert { text: "\n".into() },
|
||||
CharOperation::Insert {
|
||||
text: " ".into(),
|
||||
},
|
||||
CharOperation::Keep { bytes: 1 },
|
||||
];
|
||||
let line_ops = char_ops_to_line_ops(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
line_ops,
|
||||
vec![
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Insert { lines: 2 },
|
||||
LineOperation::Keep { lines: 2 },
|
||||
LineOperation::Delete { lines: 1 },
|
||||
LineOperation::Insert { lines: 2 },
|
||||
]
|
||||
);
|
||||
let new_text = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(
|
||||
new_text,
|
||||
apply_line_operations(old_text, &new_text, &line_ops)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_diffs() {
|
||||
random_test(|mut rng| {
|
||||
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let old = random_text(&mut rng, old_text_len);
|
||||
println!("old text: {:?}", old);
|
||||
|
||||
let new = randomly_edit(&old, &mut rng);
|
||||
println!("new text: {:?}", new);
|
||||
|
||||
let char_operations = random_streaming_diff(&mut rng, &old, &new);
|
||||
println!("char operations: {:?}", char_operations);
|
||||
|
||||
// Use apply_char_operations to verify the result
|
||||
let patched = apply_char_operations(&old, &char_operations);
|
||||
assert_eq!(patched, new);
|
||||
|
||||
// Test char_ops_to_line_ops
|
||||
let line_ops = char_ops_to_line_ops(&old, &char_operations);
|
||||
println!("line operations: {:?}", line_ops);
|
||||
let patched = apply_line_operations(&old, &new, &line_ops);
|
||||
assert_eq!(patched, new);
|
||||
});
|
||||
}
|
||||
|
||||
fn char_ops_to_line_ops(old_text: &str, char_ops: &[CharOperation]) -> Vec<LineOperation> {
|
||||
let old_rope = Rope::from(old_text);
|
||||
let mut diff = LineDiff::default();
|
||||
for op in char_ops {
|
||||
diff.push_char_operation(op, &old_rope);
|
||||
}
|
||||
diff.finish(&old_rope);
|
||||
diff.line_operations()
|
||||
}
|
||||
|
||||
fn random_streaming_diff(rng: &mut impl Rng, old: &str, new: &str) -> Vec<CharOperation> {
|
||||
let mut diff = StreamingDiff::new(old.to_string());
|
||||
let mut char_operations = Vec::new();
|
||||
let mut diff = StreamingDiff::new(old.clone());
|
||||
let mut hunks = Vec::new();
|
||||
let mut new_len = 0;
|
||||
|
||||
while new_len < new.len() {
|
||||
let mut chunk_len = rng.gen_range(1..=new.len() - new_len);
|
||||
while !new.is_char_boundary(new_len + chunk_len) {
|
||||
chunk_len += 1;
|
||||
}
|
||||
let chunk = &new[new_len..new_len + chunk_len];
|
||||
let new_hunks = diff.push_new(chunk);
|
||||
char_operations.extend(new_hunks);
|
||||
new_len += chunk_len;
|
||||
let mut new = String::new();
|
||||
while new_len < new_text_len {
|
||||
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
|
||||
let new_chunk = util::RandomCharIter::new(&mut rng)
|
||||
.take(new_len)
|
||||
.collect::<String>();
|
||||
log::info!("new chunk: {:?}", new_chunk);
|
||||
new_len += new_chunk_len;
|
||||
new.push_str(&new_chunk);
|
||||
let new_hunks = diff.push_new(&new_chunk);
|
||||
log::info!("hunks: {:?}", new_hunks);
|
||||
hunks.extend(new_hunks);
|
||||
}
|
||||
let final_hunks = diff.finish();
|
||||
log::info!("final hunks: {:?}", final_hunks);
|
||||
hunks.extend(final_hunks);
|
||||
|
||||
char_operations.extend(diff.finish());
|
||||
char_operations
|
||||
}
|
||||
|
||||
fn random_test<F>(mut test_fn: F)
|
||||
where
|
||||
F: FnMut(StdRng),
|
||||
{
|
||||
let iterations = env::var("ITERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `ITERATIONS` variable"))
|
||||
.unwrap_or(100);
|
||||
|
||||
let seed: u64 = env::var("SEED")
|
||||
.map(|s| s.parse().expect("invalid `SEED` variable"))
|
||||
.unwrap_or(0);
|
||||
|
||||
println!(
|
||||
"Running test with {} iterations and seed {}",
|
||||
iterations, seed
|
||||
);
|
||||
|
||||
for i in 0..iterations {
|
||||
println!("Iteration {}", i + 1);
|
||||
let rng = StdRng::seed_from_u64(seed + i);
|
||||
test_fn(rng);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_line_operations(old_text: &str, new_text: &str, line_ops: &[LineOperation]) -> String {
|
||||
let mut result: Vec<&str> = Vec::new();
|
||||
|
||||
let old_lines: Vec<&str> = old_text.split('\n').collect();
|
||||
let new_lines: Vec<&str> = new_text.split('\n').collect();
|
||||
let mut old_start = 0_usize;
|
||||
let mut new_start = 0_usize;
|
||||
|
||||
for op in line_ops {
|
||||
match op {
|
||||
LineOperation::Keep { lines } => {
|
||||
let old_end = old_start + *lines as usize;
|
||||
result.extend(&old_lines[old_start..old_end]);
|
||||
old_start = old_end;
|
||||
new_start += *lines as usize;
|
||||
}
|
||||
LineOperation::Delete { lines } => {
|
||||
old_start += *lines as usize;
|
||||
}
|
||||
LineOperation::Insert { lines } => {
|
||||
let new_end = new_start + *lines as usize;
|
||||
result.extend(&new_lines[new_start..new_end]);
|
||||
new_start = new_end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_char_operations() {
|
||||
let old_text = "Hello, world!";
|
||||
let char_ops = vec![
|
||||
CharOperation::Keep { bytes: 7 },
|
||||
CharOperation::Delete { bytes: 5 },
|
||||
CharOperation::Insert {
|
||||
text: "Rust".to_string(),
|
||||
},
|
||||
CharOperation::Keep { bytes: 1 },
|
||||
];
|
||||
let result = apply_char_operations(old_text, &char_ops);
|
||||
assert_eq!(result, "Hello, Rust!");
|
||||
}
|
||||
|
||||
fn random_text(rng: &mut impl Rng, length: usize) -> String {
|
||||
util::RandomCharIter::new(rng).take(length).collect()
|
||||
}
|
||||
|
||||
fn randomly_edit(text: &str, rng: &mut impl Rng) -> String {
|
||||
let mut result = String::from(text);
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
|
||||
fn random_char_range(text: &str, rng: &mut impl Rng) -> (usize, usize) {
|
||||
let mut start = rng.gen_range(0..=text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start -= 1;
|
||||
}
|
||||
let mut end = rng.gen_range(start..=text.len());
|
||||
while !text.is_char_boundary(end) {
|
||||
end += 1;
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
|
||||
for _ in 0..edit_count {
|
||||
match rng.gen_range(0..3) {
|
||||
0 => {
|
||||
// Insert
|
||||
let (pos, _) = random_char_range(&result, rng);
|
||||
let insert_len = rng.gen_range(1..=5);
|
||||
let insert_text: String = random_text(rng, insert_len);
|
||||
result.insert_str(pos, &insert_text);
|
||||
}
|
||||
1 => {
|
||||
// Delete
|
||||
if !result.is_empty() {
|
||||
let (start, end) = random_char_range(&result, rng);
|
||||
result.replace_range(start..end, "");
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
// Replace
|
||||
if !result.is_empty() {
|
||||
let (start, end) = random_char_range(&result, rng);
|
||||
let replace_len = end - start;
|
||||
let replace_text: String = random_text(rng, replace_len);
|
||||
result.replace_range(start..end, &replace_text);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn apply_char_operations(old_text: &str, char_ops: &[CharOperation]) -> String {
|
||||
let mut result = String::new();
|
||||
log::info!("new text: {:?}", new);
|
||||
let mut old_ix = 0;
|
||||
|
||||
for operation in char_ops {
|
||||
match operation {
|
||||
CharOperation::Keep { bytes } => {
|
||||
result.push_str(&old_text[old_ix..old_ix + bytes]);
|
||||
old_ix += bytes;
|
||||
let mut new_ix = 0;
|
||||
let mut patched = String::new();
|
||||
for hunk in hunks {
|
||||
match hunk {
|
||||
Hunk::Keep { len } => {
|
||||
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
|
||||
patched.push_str(&old[old_ix..old_ix + len]);
|
||||
old_ix += len;
|
||||
new_ix += len;
|
||||
}
|
||||
CharOperation::Delete { bytes } => {
|
||||
old_ix += bytes;
|
||||
Hunk::Remove { len } => {
|
||||
old_ix += len;
|
||||
}
|
||||
CharOperation::Insert { text } => {
|
||||
result.push_str(text);
|
||||
Hunk::Insert { text } => {
|
||||
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
|
||||
patched.push_str(&text);
|
||||
new_ix += text.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
assert_eq!(patched, new);
|
||||
}
|
||||
}
|
||||
|
||||
86
crates/assistant/src/system_prompts/edits.md
Normal file
@@ -0,0 +1,86 @@
|
||||
When the user asks you to suggest edits for a buffer, use a strict template consisting of:
|
||||
|
||||
* A markdown code block with the file path as the language identifier.
|
||||
* The original code that should be replaced
|
||||
* A separator line (`---`)
|
||||
* The new text that should replace the original lines
|
||||
|
||||
Each code block may only contain an edit for one single contiguous range of text. Use multiple code blocks for multiple edits.
|
||||
|
||||
## Example
|
||||
|
||||
If you have a buffer with the following lines:
|
||||
|
||||
```path/to/file.rs
|
||||
fn quicksort(arr: &mut [i32]) {
|
||||
if arr.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
let pivot_index = partition(arr);
|
||||
let (left, right) = arr.split_at_mut(pivot_index);
|
||||
quicksort(left);
|
||||
quicksort(&mut right[1..]);
|
||||
}
|
||||
|
||||
fn partition(arr: &mut [i32]) -> usize {
|
||||
let last_index = arr.len() - 1;
|
||||
let pivot = arr[last_index];
|
||||
let mut i = 0;
|
||||
for j in 0..last_index {
|
||||
if arr[j] <= pivot {
|
||||
arr.swap(i, j);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
arr.swap(i, last_index);
|
||||
i
|
||||
}
|
||||
```
|
||||
|
||||
And you want to replace the for loop inside `partition`, output the following.
|
||||
|
||||
```edit path/to/file.rs
|
||||
for j in 0..last_index {
|
||||
if arr[j] <= pivot {
|
||||
arr.swap(i, j);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
---
|
||||
let mut j = 0;
|
||||
while j < last_index {
|
||||
if arr[j] <= pivot {
|
||||
arr.swap(i, j);
|
||||
i += 1;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
```
|
||||
|
||||
If you wanted to insert comments above the partition function, output the following:
|
||||
|
||||
```edit path/to/file.rs
|
||||
fn partition(arr: &mut [i32]) -> usize {
|
||||
---
|
||||
// A helper function used for quicksort.
|
||||
fn partition(arr: &mut [i32]) -> usize {
|
||||
```
|
||||
|
||||
If you wanted to delete the partition function, output the following:
|
||||
|
||||
```edit path/to/file.rs
|
||||
fn partition(arr: &mut [i32]) -> usize {
|
||||
let last_index = arr.len() - 1;
|
||||
let pivot = arr[last_index];
|
||||
let mut i = 0;
|
||||
for j in 0..last_index {
|
||||
if arr[j] <= pivot {
|
||||
arr.swap(i, j);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
arr.swap(i, last_index);
|
||||
i
|
||||
}
|
||||
---
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
|
||||
ModelSelector, DEFAULT_CONTEXT_LINES,
|
||||
humanize_token_count, prompts::generate_terminal_assistant_prompt, AssistantPanel,
|
||||
AssistantPanelEvent, LanguageModelCompletionProvider, ModelSelector,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
@@ -16,9 +16,7 @@ use gpui::{
|
||||
Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp,
|
||||
@@ -32,13 +30,8 @@ use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
|
||||
pub fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
|
||||
cx.set_global(TerminalInlineAssistant::new(fs, telemetry));
|
||||
}
|
||||
|
||||
const PROMPT_HISTORY_MAX_LEN: usize = 20;
|
||||
@@ -60,24 +53,18 @@ pub struct TerminalInlineAssistant {
|
||||
prompt_history: VecDeque<String>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
impl Global for TerminalInlineAssistant {}
|
||||
|
||||
impl TerminalInlineAssistant {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
) -> Self {
|
||||
pub fn new(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>) -> Self {
|
||||
Self {
|
||||
next_assist_id: TerminalInlineAssistId::default(),
|
||||
assists: HashMap::default(),
|
||||
prompt_history: VecDeque::default(),
|
||||
telemetry: Some(telemetry),
|
||||
fs,
|
||||
prompt_builder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,18 +215,17 @@ impl TerminalInlineAssistant {
|
||||
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
||||
|
||||
let shell = std::env::var("SHELL").ok();
|
||||
let (latest_output, working_directory) = assist
|
||||
let working_directory = assist
|
||||
.terminal
|
||||
.update(cx, |terminal, cx| {
|
||||
let terminal = terminal.model().read(cx);
|
||||
let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
|
||||
let working_directory = terminal
|
||||
terminal
|
||||
.model()
|
||||
.read(cx)
|
||||
.working_directory()
|
||||
.map(|path| path.to_string_lossy().to_string());
|
||||
(latest_output, working_directory)
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
.flatten();
|
||||
|
||||
let context_request = if assist.include_context {
|
||||
assist.workspace.as_ref().and_then(|workspace| {
|
||||
@@ -257,7 +243,7 @@ impl TerminalInlineAssistant {
|
||||
None
|
||||
};
|
||||
|
||||
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
|
||||
let prompt = generate_terminal_assistant_prompt(
|
||||
&assist
|
||||
.prompt_editor
|
||||
.clone()
|
||||
@@ -266,8 +252,7 @@ impl TerminalInlineAssistant {
|
||||
.prompt(cx),
|
||||
shell.as_deref(),
|
||||
working_directory.as_deref(),
|
||||
&latest_output,
|
||||
)?;
|
||||
);
|
||||
|
||||
let mut messages = Vec::new();
|
||||
if let Some(context_request) = context_request {
|
||||
@@ -563,7 +548,7 @@ impl Render for PromptEditor {
|
||||
.gap_2()
|
||||
.child(ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
IconButton::new("context", IconName::SlidersAlt)
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -571,7 +556,7 @@ impl Render for PromptEditor {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
@@ -715,9 +700,6 @@ impl PromptEditor {
|
||||
|
||||
fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let assist_id = self.id;
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
self.pending_token_count = cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||
let request =
|
||||
@@ -725,7 +707,11 @@ impl PromptEditor {
|
||||
inline_assistant.request_for_inline_assist(assist_id, cx)
|
||||
})??;
|
||||
|
||||
let token_count = cx.update(|cx| model.count_tokens(request, cx))?.await?;
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
@@ -854,7 +840,7 @@ impl PromptEditor {
|
||||
}
|
||||
|
||||
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
let model = LanguageModelRegistry::read_global(cx).active_model()?;
|
||||
let model = LanguageModelCompletionProvider::read_global(cx).active_model()?;
|
||||
let token_count = self.token_count?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
@@ -920,7 +906,6 @@ impl PromptEditor {
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
@@ -958,7 +943,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
|
||||
// Ensure that the assistant cannot accidently execute commands that are streamed into the terminal
|
||||
let input = hunk.replace(CARRIAGE_RETURN, " ");
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(input));
|
||||
@@ -996,16 +981,19 @@ impl Codegen {
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
|
||||
let telemetry = self.telemetry.clone();
|
||||
let model_telemetry_id = LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.telemetry_id())
|
||||
.unwrap_or_default();
|
||||
let response =
|
||||
LanguageModelCompletionProvider::read_global(cx).stream_completion(prompt, cx);
|
||||
|
||||
self.generation = cx.spawn(|this, mut cx| async move {
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let response = model.stream_completion(prompt, &cx).await;
|
||||
let response = response.await;
|
||||
let generate = async {
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
### Using the Assistant
|
||||
|
||||
Once you have configured a provider, you can interact with the provider's language models in a context editor.
|
||||
|
||||
To create a new context editor, use the menu in the top right of the assistant panel and the `New Context` option.
|
||||
|
||||
In the context editor, select a model from one of the configured providers, type a message in the `You` block, and submit with `cmd-enter` (or `ctrl-enter` on Linux).
|
||||
|
||||
### Inline assistant
|
||||
|
||||
When you're in a normal editor, you can use `ctrl-enter` to open the inline assistant.
|
||||
|
||||
The inline assistant allows you to send the current selection (or the current line) to a language model and modify the selection with the language model's response.
|
||||
|
||||
### Adding Prompts
|
||||
|
||||
You can customize the default prompts that are used in new context editor, by opening the `Prompt Library`.
|
||||
|
||||
Open the `Prompt Library` using either the menu in the top right of the assistant panel and choosing the `Prompt Library` option, or by using the `assistant: deploy prompt library` command when the assistant panel is focused.
|
||||
|
||||
### Viewing past contexts
|
||||
|
||||
You view all previous contexts by opening up the `History` tab in the assistant panel.
|
||||
|
||||
Open the `History` using the menu in the top right of the assistant panel and choosing the `History`.
|
||||
@@ -49,7 +49,7 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
//
|
||||
// It may be that `LspAdapterDelegate` needs a more general name, or
|
||||
// perhaps another kind of delegate is needed here.
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>>;
|
||||
}
|
||||
@@ -60,7 +60,7 @@ pub type RenderFoldPlaceholder = Arc<
|
||||
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||
>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Default)]
|
||||
pub struct SlashCommandOutput {
|
||||
pub text: String,
|
||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
|
||||
33
crates/assistant_tooling/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "assistant_tooling"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_tooling.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
repair_json.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sum_tree.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
1
crates/assistant_tooling/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
85
crates/assistant_tooling/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Assistant Tooling
|
||||
|
||||
Bringing Language Model tool calling to GPUI.
|
||||
|
||||
This unlocks:
|
||||
|
||||
- **Structured Extraction** of model responses
|
||||
- **Validation** of model inputs
|
||||
- **Execution** of chosen tools
|
||||
|
||||
## Overview
|
||||
|
||||
Language Models can produce structured outputs that are perfect for calling functions. The most famous of these is OpenAI's tool calling. When making a chat completion you can pass a list of tools available to the model. The model will choose `0..n` tools to help them complete a user's task. It's up to _you_ to create the tools that the model can call.
|
||||
|
||||
> **User**: "Hey I need help with implementing a collapsible panel in GPUI"
|
||||
>
|
||||
> **Assistant**: "Sure, I can help with that. Let me see what I can find."
|
||||
>
|
||||
> `tool_calls: ["name": "query_codebase", arguments: "{ 'query': 'GPUI collapsible panel' }"]`
|
||||
>
|
||||
> `result: "['crates/gpui/src/panel.rs:12: impl Panel { ... }', 'crates/gpui/src/panel.rs:20: impl Panel { ... }']"`
|
||||
>
|
||||
> **Assistant**: "Here are some excerpts from the GPUI codebase that might help you."
|
||||
|
||||
This library is designed to facilitate this interaction mode by allowing you to go from `struct` to `tool` with two simple traits, `LanguageModelTool` and `ToolView`.
|
||||
|
||||
## Using the Tool Registry
|
||||
|
||||
```rust
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(WeatherTool { api_client },
|
||||
})
|
||||
.unwrap(); // You can only register one tool per name
|
||||
|
||||
let completion = cx.update(|cx| {
|
||||
CompletionProvider::get(cx).complete(
|
||||
model_name,
|
||||
messages,
|
||||
Vec::new(),
|
||||
1.0,
|
||||
// The definitions get passed directly to OpenAI when you want
|
||||
// the model to be able to call your tool
|
||||
tool_registry.definitions(),
|
||||
)
|
||||
});
|
||||
|
||||
let mut stream = completion?.await?;
|
||||
|
||||
let mut message = AssistantMessage::new();
|
||||
|
||||
while let Some(delta) = stream.next().await {
|
||||
// As messages stream in, you'll get both assistant content
|
||||
if let Some(content) = &delta.content {
|
||||
message
|
||||
.body
|
||||
.update(cx, |message, cx| message.append(&content, cx));
|
||||
}
|
||||
|
||||
// And tool calls!
|
||||
for tool_call_delta in delta.tool_calls {
|
||||
let index = tool_call_delta.index as usize;
|
||||
if index >= message.tool_calls.len() {
|
||||
message.tool_calls.resize_with(index + 1, Default::default);
|
||||
}
|
||||
let tool_call = &mut message.tool_calls[index];
|
||||
|
||||
// Build up an ID
|
||||
if let Some(id) = &tool_call_delta.id {
|
||||
tool_call.id.push_str(id);
|
||||
}
|
||||
|
||||
tool_registry.update_tool_call(
|
||||
tool_call,
|
||||
tool_call_delta.name.as_deref(),
|
||||
tool_call_delta.arguments.as_deref(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once the stream of tokens is complete, you can exexute the tool call by calling `tool_registry.execute_tool_call(tool_call, cx)`, which returns a `Task<Result<()>>`.
|
||||
|
||||
As the tokens stream in and tool calls are executed, your `ToolView` will get updates. Render each tool call by passing that `tool_call` in to `tool_registry.render_tool_call(tool_call, cx)`. The final message for the model can be pulled by calling `self.tool_registry.content_for_tool_call( tool_call, &mut project_context, cx, )`.
|
||||
13
crates/assistant_tooling/src/assistant_tooling.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
mod attachment_registry;
|
||||
mod project_context;
|
||||
mod tool_registry;
|
||||
|
||||
pub use attachment_registry::{
|
||||
AttachmentOutput, AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment,
|
||||
UserAttachment,
|
||||
};
|
||||
pub use project_context::ProjectContext;
|
||||
pub use tool_registry::{
|
||||
LanguageModelTool, SavedToolFunctionCall, ToolFunctionCall, ToolFunctionDefinition,
|
||||
ToolRegistry, ToolView,
|
||||
};
|
||||
234
crates/assistant_tooling/src/attachment_registry.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use futures::future::join_all;
|
||||
use gpui::{AnyView, Render, Task, View, WindowContext};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct AttachmentRegistry {
|
||||
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
|
||||
}
|
||||
|
||||
pub trait AttachmentOutput {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
}
|
||||
|
||||
pub trait LanguageModelAttachment {
|
||||
type Output: DeserializeOwned + Serialize + 'static;
|
||||
type View: Render + AttachmentOutput;
|
||||
|
||||
fn name(&self) -> Arc<str>;
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
/// A collected attachment from running an attachment tool
|
||||
pub struct UserAttachment {
|
||||
pub view: AnyView,
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedUserAttachment {
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
}
|
||||
|
||||
/// Internal representation of an attachment tool to allow us to treat them dynamically
|
||||
struct RegisteredAttachment {
|
||||
name: Arc<str>,
|
||||
enabled: AtomicBool,
|
||||
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
|
||||
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
|
||||
}
|
||||
|
||||
impl AttachmentRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_attachments: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
|
||||
let attachment = Arc::new(attachment);
|
||||
|
||||
let call = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
let result = attachment.run(cx);
|
||||
let attachment = attachment.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<A::Output> = result.await;
|
||||
let serialized_output =
|
||||
result
|
||||
.as_ref()
|
||||
.map_err(ToString::to_string)
|
||||
.and_then(|output| {
|
||||
Ok(RawValue::from_string(
|
||||
serde_json::to_string(output).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.unwrap())
|
||||
});
|
||||
|
||||
let view = cx.update(|cx| attachment.view(result, cx))?;
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: attachment.name(),
|
||||
view: view.into(),
|
||||
generate_fn: generate::<A>,
|
||||
serialized_output,
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let deserialize = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
|
||||
let serialized_output = saved_attachment.serialized_output.clone();
|
||||
let output = match &serialized_output {
|
||||
Ok(serialized_output) => {
|
||||
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
|
||||
}
|
||||
Err(error) => Err(anyhow!("{error}")),
|
||||
};
|
||||
let view = attachment.view(output, cx).into();
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: saved_attachment.name.clone(),
|
||||
view,
|
||||
serialized_output,
|
||||
generate_fn: generate::<A>,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
self.registered_attachments.insert(
|
||||
TypeId::of::<A>(),
|
||||
RegisteredAttachment {
|
||||
name: attachment.name(),
|
||||
call,
|
||||
deserialize,
|
||||
enabled: AtomicBool::new(true),
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
||||
fn generate<T: LanguageModelAttachment>(
|
||||
view: AnyView,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
view.downcast::<T::View>()
|
||||
.unwrap()
|
||||
.update(cx, |view, cx| T::View::generate(view, project, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
is_enabled: bool,
|
||||
) {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.store(is_enabled, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(&self) -> bool {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.load(SeqCst)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<UserAttachment>> {
|
||||
let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) else {
|
||||
return Task::ready(Err(anyhow!("no attachment tool")));
|
||||
};
|
||||
|
||||
(attachment.call)(cx)
|
||||
}
|
||||
|
||||
pub fn call_all_attachment_tools(
|
||||
self: Arc<Self>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Task<Result<Vec<UserAttachment>>> {
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let attachment_tasks = cx.update(|cx| {
|
||||
let mut tasks = Vec::new();
|
||||
for attachment in this
|
||||
.registered_attachments
|
||||
.values()
|
||||
.filter(|attachment| attachment.enabled.load(SeqCst))
|
||||
{
|
||||
tasks.push((attachment.call)(cx))
|
||||
}
|
||||
|
||||
tasks
|
||||
})?;
|
||||
|
||||
let attachments = join_all(attachment_tasks.into_iter()).await;
|
||||
|
||||
Ok(attachments
|
||||
.into_iter()
|
||||
.filter_map(|attachment| attachment.log_err())
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_user_attachment(
|
||||
&self,
|
||||
user_attachment: &UserAttachment,
|
||||
) -> SavedUserAttachment {
|
||||
SavedUserAttachment {
|
||||
name: user_attachment.name.clone(),
|
||||
serialized_output: user_attachment.serialized_output.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_user_attachment(
|
||||
&self,
|
||||
saved_user_attachment: SavedUserAttachment,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<UserAttachment> {
|
||||
if let Some(registered_attachment) = self
|
||||
.registered_attachments
|
||||
.values()
|
||||
.find(|attachment| attachment.name == saved_user_attachment.name)
|
||||
{
|
||||
(registered_attachment.deserialize)(&saved_user_attachment, cx)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"no attachment tool for name {}",
|
||||
saved_user_attachment.name
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAttachment {
|
||||
pub fn generate(&self, output: &mut ProjectContext, cx: &mut WindowContext) -> Option<String> {
|
||||
let result = (self.generate_fn)(self.view.clone(), output, cx);
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
296
crates/assistant_tooling/src/project_context.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use project::{Fs, Project, ProjectPath, Worktree};
|
||||
use std::{cmp::Ordering, fmt::Write as _, ops::Range, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
|
||||
pub struct ProjectContext {
|
||||
files: TreeMap<ProjectPath, PathState>,
|
||||
project: WeakModel<Project>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum PathState {
|
||||
PathOnly,
|
||||
EntireFile,
|
||||
Excerpts { ranges: Vec<Range<usize>> },
|
||||
}
|
||||
|
||||
impl ProjectContext {
|
||||
pub fn new(project: WeakModel<Project>, fs: Arc<dyn Fs>) -> Self {
|
||||
Self {
|
||||
files: TreeMap::default(),
|
||||
fs,
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path(&mut self, project_path: ProjectPath) {
|
||||
if self.files.get(&project_path).is_none() {
|
||||
self.files.insert(project_path, PathState::PathOnly);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_excerpts(&mut self, project_path: ProjectPath, new_ranges: &[Range<usize>]) {
|
||||
let previous_state = self
|
||||
.files
|
||||
.get(&project_path)
|
||||
.unwrap_or(&PathState::PathOnly);
|
||||
|
||||
let mut ranges = match previous_state {
|
||||
PathState::EntireFile => return,
|
||||
PathState::PathOnly => Vec::new(),
|
||||
PathState::Excerpts { ranges } => ranges.to_vec(),
|
||||
};
|
||||
|
||||
for new_range in new_ranges {
|
||||
let ix = ranges.binary_search_by(|probe| {
|
||||
if probe.end < new_range.start {
|
||||
Ordering::Less
|
||||
} else if probe.start > new_range.end {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
|
||||
match ix {
|
||||
Ok(mut ix) => {
|
||||
let existing = &mut ranges[ix];
|
||||
existing.start = existing.start.min(new_range.start);
|
||||
existing.end = existing.end.max(new_range.end);
|
||||
while ix + 1 < ranges.len() && ranges[ix + 1].start <= ranges[ix].end {
|
||||
ranges[ix].end = ranges[ix].end.max(ranges[ix + 1].end);
|
||||
ranges.remove(ix + 1);
|
||||
}
|
||||
while ix > 0 && ranges[ix - 1].end >= ranges[ix].start {
|
||||
ranges[ix].start = ranges[ix].start.min(ranges[ix - 1].start);
|
||||
ranges.remove(ix - 1);
|
||||
ix -= 1;
|
||||
}
|
||||
}
|
||||
Err(ix) => {
|
||||
ranges.insert(ix, new_range.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files
|
||||
.insert(project_path, PathState::Excerpts { ranges });
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, project_path: ProjectPath) {
|
||||
self.files.insert(project_path, PathState::EntireFile);
|
||||
}
|
||||
|
||||
pub fn generate_system_message(&self, cx: &mut AppContext) -> Task<Result<String>> {
|
||||
let project = self
|
||||
.project
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("project dropped"));
|
||||
let files = self.files.clone();
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let project = project?;
|
||||
let mut result = "project structure:\n".to_string();
|
||||
|
||||
let mut last_worktree: Option<Model<Worktree>> = None;
|
||||
for (project_path, path_state) in files.iter() {
|
||||
if let Some(worktree) = &last_worktree {
|
||||
if worktree.read_with(&cx, |tree, _| tree.id())? != project_path.worktree_id {
|
||||
last_worktree = None;
|
||||
}
|
||||
}
|
||||
|
||||
let worktree;
|
||||
if let Some(last_worktree) = &last_worktree {
|
||||
worktree = last_worktree.clone();
|
||||
} else if let Some(tree) = project.read_with(&cx, |project, cx| {
|
||||
project.worktree_for_id(project_path.worktree_id, cx)
|
||||
})? {
|
||||
worktree = tree;
|
||||
last_worktree = Some(worktree.clone());
|
||||
let worktree_name =
|
||||
worktree.read_with(&cx, |tree, _cx| tree.root_name().to_string())?;
|
||||
writeln!(&mut result, "# {}", worktree_name).unwrap();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let worktree_abs_path = worktree.read_with(&cx, |tree, _cx| tree.abs_path())?;
|
||||
let path = &project_path.path;
|
||||
writeln!(&mut result, "## {}", path.display()).unwrap();
|
||||
|
||||
match path_state {
|
||||
PathState::PathOnly => {}
|
||||
PathState::EntireFile => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
writeln!(&mut result, "~~~\n{text}\n~~~").unwrap();
|
||||
}
|
||||
PathState::Excerpts { ranges } => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
|
||||
// Assumption: ranges are in order, not overlapping
|
||||
let mut prev_range_end = 0;
|
||||
for range in ranges {
|
||||
if range.start > prev_range_end {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
prev_range_end = range.end;
|
||||
}
|
||||
|
||||
let mut start = range.start;
|
||||
let mut end = range.end.min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
result.push_str(&text[start..end]);
|
||||
if !result.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if prev_range_end < text.len() {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
}
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_system_message_generation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let file_3_contents = r#"
|
||||
fn test1() {}
|
||||
fn test2() {}
|
||||
fn test3() {}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
json!({
|
||||
"root1": {
|
||||
"lib": {
|
||||
"file1.rs": "mod example;",
|
||||
"file2.rs": "",
|
||||
},
|
||||
"test": {
|
||||
"file3.rs": file_3_contents,
|
||||
}
|
||||
},
|
||||
"root2": {
|
||||
"src": {
|
||||
"main.rs": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs.clone(),
|
||||
["/code/root1".as_ref(), "/code/root2".as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let worktree_ids = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let mut ax = ProjectContext::new(project.downgrade(), fs);
|
||||
|
||||
ax.add_file(ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("lib/file1.rs").into(),
|
||||
});
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
|
||||
ax.add_excerpts(
|
||||
ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("test/file3.rs").into(),
|
||||
},
|
||||
&[
|
||||
file_3_contents.find("fn test2").unwrap()
|
||||
..file_3_contents.find("fn test3").unwrap(),
|
||||
],
|
||||
);
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
## test/file3.rs
|
||||
~~~
|
||||
...
|
||||
fn test2() {}
|
||||
...
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
526
crates/assistant_tooling/src/tool_registry.rs
Normal file
@@ -0,0 +1,526 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AnyElement, AnyView, IntoElement, Render, Task, View, WindowContext};
|
||||
use repair_json::repair;
|
||||
use schemars::{schema::RootSchema, schema_for, JsonSchema};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
mem,
|
||||
sync::atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
use ui::ViewContext;
|
||||
|
||||
pub struct ToolRegistry {
|
||||
registered_tools: HashMap<String, RegisteredTool>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ToolFunctionCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
state: ToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum ToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool(Box<dyn InternalToolView>),
|
||||
ExecutedTool(Box<dyn InternalToolView>),
|
||||
}
|
||||
|
||||
trait InternalToolView {
|
||||
fn view(&self) -> AnyView;
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext);
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>>;
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>>;
|
||||
fn deserialize_output(&self, raw_value: &RawValue, cx: &mut WindowContext) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct SavedToolFunctionCall {
|
||||
id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
state: SavedToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
enum SavedToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool,
|
||||
ExecutedTool(Box<RawValue>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ToolFunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: RootSchema,
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool {
|
||||
type View: ToolView;
|
||||
|
||||
/// Returns the name of the tool.
|
||||
///
|
||||
/// This name is exposed to the language model to allow the model to pick
|
||||
/// which tools to use. As this name is used to identify the tool within a
|
||||
/// tool registry, it should be unique.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the description of the tool.
|
||||
///
|
||||
/// This can be used to _prompt_ the model as to what the tool does.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
|
||||
fn definition(&self) -> ToolFunctionDefinition {
|
||||
let root_schema = schema_for!(<Self::View as ToolView>::Input);
|
||||
|
||||
ToolFunctionDefinition {
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
parameters: root_schema,
|
||||
}
|
||||
}
|
||||
|
||||
/// A view of the output of running the tool, for displaying to the user.
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
pub trait ToolView: Render {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: DeserializeOwned + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type SerializedState: DeserializeOwned + Serialize;
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut ViewContext<Self>) -> String;
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>);
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
|
||||
|
||||
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState;
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
struct RegisteredTool {
|
||||
enabled: AtomicBool,
|
||||
type_id: TypeId,
|
||||
build_view: Box<dyn Fn(&mut WindowContext) -> Box<dyn InternalToolView>>,
|
||||
definition: ToolFunctionDefinition,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_enabled<T: 'static + LanguageModelTool>(&self, is_enabled: bool) {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
tool.enabled.store(is_enabled, SeqCst);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tool_enabled<T: 'static + LanguageModelTool>(&self) -> bool {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
return tool.enabled.load(SeqCst);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> Vec<ToolFunctionDefinition> {
|
||||
self.registered_tools
|
||||
.values()
|
||||
.filter(|tool| tool.enabled.load(SeqCst))
|
||||
.map(|tool| tool.definition.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update_tool_call(
|
||||
&self,
|
||||
call: &mut ToolFunctionCall,
|
||||
name: Option<&str>,
|
||||
arguments: Option<&str>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(name) = name {
|
||||
call.name.push_str(name);
|
||||
}
|
||||
if let Some(arguments) = arguments {
|
||||
if call.arguments.is_empty() {
|
||||
if let Some(tool) = self.registered_tools.get(&call.name) {
|
||||
let view = (tool.build_view)(cx);
|
||||
call.state = ToolFunctionCallState::KnownTool(view);
|
||||
} else {
|
||||
call.state = ToolFunctionCallState::NoSuchTool;
|
||||
}
|
||||
}
|
||||
call.arguments.push_str(arguments);
|
||||
|
||||
if let ToolFunctionCallState::KnownTool(view) = &call.state {
|
||||
if let Ok(repaired_arguments) = repair(call.arguments.clone()) {
|
||||
view.try_set_input(&repaired_arguments, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_tool_call(
|
||||
&self,
|
||||
tool_call: &mut ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if let ToolFunctionCallState::KnownTool(view) = mem::take(&mut tool_call.state) {
|
||||
let task = view.execute(cx);
|
||||
tool_call.state = ToolFunctionCallState::ExecutedTool(view);
|
||||
Some(task)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
Some(ui::Label::new("No such tool").into_any_element())
|
||||
}
|
||||
ToolFunctionCallState::Initializing => None,
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
Some(view.view().into_any_element())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_for_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
project_context: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::Initializing => String::new(),
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
format!("No such tool: {}", tool_call.name)
|
||||
}
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
view.generate(project_context, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_tool_call(
|
||||
&self,
|
||||
call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<SavedToolFunctionCall> {
|
||||
Ok(SavedToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
ToolFunctionCallState::Initializing => SavedToolFunctionCallState::Initializing,
|
||||
ToolFunctionCallState::NoSuchTool => SavedToolFunctionCallState::NoSuchTool,
|
||||
ToolFunctionCallState::KnownTool(_) => SavedToolFunctionCallState::KnownTool,
|
||||
ToolFunctionCallState::ExecutedTool(view) => {
|
||||
SavedToolFunctionCallState::ExecutedTool(view.serialize_output(cx)?)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize_tool_call(
|
||||
&self,
|
||||
call: &SavedToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<ToolFunctionCall> {
|
||||
let Some(tool) = self.registered_tools.get(&call.name) else {
|
||||
return Err(anyhow!("no such tool {}", call.name));
|
||||
};
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
SavedToolFunctionCallState::Initializing => ToolFunctionCallState::Initializing,
|
||||
SavedToolFunctionCallState::NoSuchTool => ToolFunctionCallState::NoSuchTool,
|
||||
SavedToolFunctionCallState::KnownTool => {
|
||||
log::error!("Deserialized tool that had not executed");
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
ToolFunctionCallState::KnownTool(view)
|
||||
}
|
||||
SavedToolFunctionCallState::ExecutedTool(output) => {
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
view.deserialize_output(output, cx)?;
|
||||
ToolFunctionCallState::ExecutedTool(view)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
|
||||
let name = tool.name();
|
||||
let registered_tool = RegisteredTool {
|
||||
type_id: TypeId::of::<T>(),
|
||||
definition: tool.definition(),
|
||||
enabled: AtomicBool::new(true),
|
||||
build_view: Box::new(move |cx: &mut WindowContext| Box::new(tool.view(cx))),
|
||||
};
|
||||
|
||||
let previous = self.registered_tools.insert(name.clone(), registered_tool);
|
||||
if previous.is_some() {
|
||||
return Err(anyhow!("already registered a tool with name {}", name));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToolView> InternalToolView for View<T> {
|
||||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
|
||||
self.update(cx, |view, cx| view.generate(project, cx))
|
||||
}
|
||||
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext) {
|
||||
if let Ok(input) = serde_json::from_str::<T::Input>(input) {
|
||||
self.update(cx, |view, cx| {
|
||||
view.set_input(input, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>> {
|
||||
self.update(cx, |view, cx| view.execute(cx))
|
||||
}
|
||||
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>> {
|
||||
let output = self.update(cx, |view, cx| view.serialize(cx));
|
||||
Ok(RawValue::from_string(serde_json::to_string(&output)?)?)
|
||||
}
|
||||
|
||||
fn deserialize_output(&self, output: &RawValue, cx: &mut WindowContext) -> Result<()> {
|
||||
let state = serde_json::from_str::<T::SerializedState>(output.get())?;
|
||||
self.update(cx, |view, cx| view.deserialize(state, cx))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ToolFunctionDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let schema = serde_json::to_string(&self.parameters).ok();
|
||||
let schema = schema.unwrap_or("None".to_string());
|
||||
write!(f, "Name: {}:\n", self.name)?;
|
||||
write!(f, "Description: {}\n", self.description)?;
|
||||
write!(f, "Parameters: {}", schema)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{div, prelude::*, Render, TestAppContext};
|
||||
use gpui::{EmptyView, View};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
struct WeatherQuery {
|
||||
location: String,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct WeatherResult {
|
||||
location: String,
|
||||
temperature: f64,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherView {
|
||||
input: Option<WeatherQuery>,
|
||||
result: Option<WeatherResult>,
|
||||
|
||||
// Fake API call
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
impl WeatherView {
|
||||
fn new(current_weather: WeatherResult) -> Self {
|
||||
Self {
|
||||
input: None,
|
||||
result: None,
|
||||
current_weather,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for WeatherView {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
match self.result {
|
||||
Some(ref result) => div()
|
||||
.child(format!("temperature: {}", result.temperature))
|
||||
.into_any_element(),
|
||||
None => div().child("Calculating weather...").into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolView for WeatherView {
|
||||
type Input = WeatherQuery;
|
||||
|
||||
type SerializedState = WeatherResult;
|
||||
|
||||
fn generate(&self, _output: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
|
||||
serde_json::to_string(&self.result).unwrap()
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
self.input = Some(input);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
let input = self.input.as_ref().unwrap();
|
||||
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
self.result = Some(weather);
|
||||
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
self.current_weather.clone()
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
self.current_weather = output;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WeatherTool {
|
||||
type View = WeatherView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"get_current_weather".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Fetches the current weather for a given location.".to_string()
|
||||
}
|
||||
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| WeatherView::new(self.current_weather.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
|
||||
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry
|
||||
.register(WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let definitions = registry.definitions();
|
||||
assert_eq!(
|
||||
definitions,
|
||||
[ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: serde_json::from_value(json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}))
|
||||
.unwrap(),
|
||||
}]
|
||||
);
|
||||
|
||||
let mut call = ToolFunctionCall {
|
||||
id: "the-id".to_string(),
|
||||
name: "get_cur".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let task = cx.update(|cx| {
|
||||
registry.update_tool_call(
|
||||
&mut call,
|
||||
Some("rent_weather"),
|
||||
Some(r#"{"location": "San Francisco","#),
|
||||
cx,
|
||||
);
|
||||
registry.update_tool_call(&mut call, None, Some(r#" "unit": "Celsius"}"#), cx);
|
||||
registry.execute_tool_call(&mut call, cx).unwrap()
|
||||
});
|
||||
task.await.unwrap();
|
||||
|
||||
match &call.state {
|
||||
ToolFunctionCallState::ExecutedTool(_view) => {}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,6 @@ struct UpdateRequestBody {
|
||||
installation_id: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
telemetry: bool,
|
||||
is_staff: Option<bool>,
|
||||
destination: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
@@ -577,27 +575,18 @@ async fn download_remote_server_binary(
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let mut target_file = File::create(&target_path).await?;
|
||||
let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
|
||||
let telemetry = Client::global(cx).telemetry().clone();
|
||||
let is_staff = telemetry.is_staff();
|
||||
let installation_id = telemetry.installation_id();
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry_enabled,
|
||||
is_staff,
|
||||
)
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry: telemetry_enabled,
|
||||
is_staff,
|
||||
destination: "remote",
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
@@ -613,28 +602,19 @@ async fn download_release(
|
||||
) -> Result<()> {
|
||||
let mut target_file = File::create(&target_path).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
|
||||
let telemetry = Client::global(cx).telemetry().clone();
|
||||
let is_staff = telemetry.is_staff();
|
||||
let installation_id = telemetry.installation_id();
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry_enabled,
|
||||
is_staff,
|
||||
)
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry: telemetry_enabled,
|
||||
is_staff,
|
||||
destination: "local",
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
|
||||
@@ -493,7 +493,7 @@ impl Room {
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade() {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?;
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
@@ -942,7 +942,7 @@ impl Room {
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
let _ = this.leave(cx).detach();
|
||||
let _ = this.leave(cx);
|
||||
}
|
||||
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
|
||||
@@ -5,13 +5,14 @@ use clap::Parser;
|
||||
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitStatus,
|
||||
sync::Arc,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use util::paths::PathWithPosition;
|
||||
use util::paths::PathLikeWithPosition;
|
||||
|
||||
struct Detect;
|
||||
|
||||
@@ -53,10 +54,13 @@ struct Args {
|
||||
}
|
||||
|
||||
fn parse_path_with_position(argument_str: &str) -> Result<String, std::io::Error> {
|
||||
let path = PathWithPosition::parse_str(argument_str);
|
||||
let path_like = PathLikeWithPosition::parse_str::<Infallible>(argument_str, |_, path_str| {
|
||||
Ok(Path::new(path_str).to_path_buf())
|
||||
})
|
||||
.unwrap();
|
||||
let curdir = env::current_dir()?;
|
||||
|
||||
let canonicalized = path.map_path(|path| match fs::canonicalize(&path) {
|
||||
let canonicalized = path_like.map_path_like(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
|
||||
@@ -7,9 +7,8 @@ pub mod user;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use async_tungstenite::tungstenite::{
|
||||
client::IntoClientRequest,
|
||||
error::Error as WebsocketError,
|
||||
http::{HeaderValue, Request, StatusCode},
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
@@ -236,8 +235,6 @@ pub enum EstablishConnectionError {
|
||||
#[error("{0}")]
|
||||
Http(#[from] http_client::Error),
|
||||
#[error("{0}")]
|
||||
InvalidHeaderValue(#[from] async_tungstenite::tungstenite::http::header::InvalidHeaderValue),
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
Websocket(#[from] async_tungstenite::tungstenite::http::Error),
|
||||
@@ -541,16 +538,9 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn production(cx: &mut AppContext) -> Arc<Self> {
|
||||
let user_agent = format!(
|
||||
"Zed/{} ({}; {})",
|
||||
AppVersion::global(cx),
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
);
|
||||
let clock = Arc::new(clock::RealSystemClock);
|
||||
let http = Arc::new(HttpClientWithUrl::new(
|
||||
&ClientSettings::get_global(cx).server_url,
|
||||
Some(user_agent),
|
||||
ProxySettings::get_global(cx).proxy.clone(),
|
||||
));
|
||||
Self::new(clock, http.clone(), cx)
|
||||
@@ -1169,24 +1159,19 @@ impl Client {
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let request = Request::builder()
|
||||
.header("Authorization", credentials.authorization_header())
|
||||
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION)
|
||||
.header("x-zed-app-version", app_version)
|
||||
.header(
|
||||
"x-zed-release-channel",
|
||||
release_channel.map(|r| r.dev_name()).unwrap_or("unknown"),
|
||||
);
|
||||
|
||||
let http = self.http.clone();
|
||||
let credentials = credentials.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
cx.background_executor().spawn(async move {
|
||||
use HttpOrHttps::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HttpOrHttps {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
let mut rpc_url = rpc_url.await?;
|
||||
let url_scheme = match rpc_url.scheme() {
|
||||
"https" => Https,
|
||||
"http" => Http,
|
||||
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
|
||||
};
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1195,37 +1180,10 @@ impl Client {
|
||||
|
||||
log::info!("connected to rpc endpoint {}", rpc_url);
|
||||
|
||||
rpc_url
|
||||
.set_scheme(match url_scheme {
|
||||
Https => "wss",
|
||||
Http => "ws",
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// We call `into_client_request` to let `tungstenite` construct the WebSocket request
|
||||
// for us from the RPC URL.
|
||||
//
|
||||
// Among other things, it will generate and set a `Sec-WebSocket-Key` header for us.
|
||||
let mut request = rpc_url.into_client_request()?;
|
||||
|
||||
// We then modify the request to add our desired headers.
|
||||
let request_headers = request.headers_mut();
|
||||
request_headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&credentials.authorization_header())?,
|
||||
);
|
||||
request_headers.insert(
|
||||
"x-zed-protocol-version",
|
||||
HeaderValue::from_str(&rpc::PROTOCOL_VERSION.to_string())?,
|
||||
);
|
||||
request_headers.insert("x-zed-app-version", HeaderValue::from_str(&app_version)?);
|
||||
request_headers.insert(
|
||||
"x-zed-release-channel",
|
||||
HeaderValue::from_str(&release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||
);
|
||||
|
||||
match url_scheme {
|
||||
Https => {
|
||||
match rpc_url.scheme() {
|
||||
"https" => {
|
||||
rpc_url.set_scheme("wss").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
let (stream, _) =
|
||||
async_tungstenite::async_std::client_async_tls(request, stream).await?;
|
||||
Ok(Connection::new(
|
||||
@@ -1234,7 +1192,9 @@ impl Client {
|
||||
.sink_map_err(|error| anyhow!(error)),
|
||||
))
|
||||
}
|
||||
Http => {
|
||||
"http" => {
|
||||
rpc_url.set_scheme("ws").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
let (stream, _) = async_tungstenite::client_async(request, stream).await?;
|
||||
Ok(Connection::new(
|
||||
stream
|
||||
@@ -1242,6 +1202,7 @@ impl Client {
|
||||
.sink_map_err(|error| anyhow!(error)),
|
||||
))
|
||||
}
|
||||
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CpuEvent, EditEvent,
|
||||
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent,
|
||||
MemoryEvent, ReplEvent, SettingEvent,
|
||||
MemoryEvent, SettingEvent,
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -531,21 +531,6 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_repl_event(
|
||||
self: &Arc<Self>,
|
||||
kernel_language: String,
|
||||
kernel_status: String,
|
||||
repl_session_id: String,
|
||||
) {
|
||||
let event = Event::Repl(ReplEvent {
|
||||
kernel_language,
|
||||
kernel_status,
|
||||
repl_session_id,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ pub struct UserStore {
|
||||
by_github_login: HashMap<String, u64>,
|
||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
||||
current_plan: Option<proto::Plan>,
|
||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||
contacts: Vec<Arc<Contact>>,
|
||||
incoming_contact_requests: Vec<Arc<User>>,
|
||||
@@ -140,7 +139,6 @@ impl UserStore {
|
||||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
|
||||
let rpc_subscriptions = vec![
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_plan),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_contacts),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_show_contacts),
|
||||
@@ -149,7 +147,6 @@ impl UserStore {
|
||||
users: Default::default(),
|
||||
by_github_login: Default::default(),
|
||||
current_user: current_user_rx,
|
||||
current_plan: None,
|
||||
contacts: Default::default(),
|
||||
incoming_contact_requests: Default::default(),
|
||||
participant_indices: Default::default(),
|
||||
@@ -283,18 +280,6 @@ impl UserStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_update_plan(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::UpdateUserPlan>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.current_plan = Some(message.payload.plan());
|
||||
cx.notify();
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_contacts(
|
||||
&mut self,
|
||||
message: UpdateContacts,
|
||||
@@ -672,10 +657,6 @@ impl UserStore {
|
||||
self.current_user.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn current_plan(&self) -> Option<proto::Plan> {
|
||||
self.current_plan
|
||||
}
|
||||
|
||||
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
||||
self.current_user.clone()
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@ BLOB_STORE_URL = "http://127.0.0.1:9000"
|
||||
BLOB_STORE_REGION = "the-region"
|
||||
ZED_CLIENT_CHECKSUM_SEED = "development-checksum-seed"
|
||||
SEED_PATH = "crates/collab/seed.default.json"
|
||||
LLM_DATABASE_URL = "postgres://postgres@localhost/zed_llm"
|
||||
LLM_DATABASE_MAX_CONNECTIONS = 5
|
||||
LLM_API_SECRET = "llm-secret"
|
||||
|
||||
# CLICKHOUSE_URL = ""
|
||||
# CLICKHOUSE_USER = "default"
|
||||
|
||||
@@ -20,7 +20,6 @@ test-support = ["sqlite"]
|
||||
[dependencies]
|
||||
anthropic.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-stripe.workspace = true
|
||||
async-tungstenite.workspace = true
|
||||
aws-config = { version = "1.1.5" }
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
@@ -31,13 +30,12 @@ chrono.workspace = true
|
||||
clock.workspace = true
|
||||
clickhouse.workspace = true
|
||||
collections.workspace = true
|
||||
dashmap.workspace = true
|
||||
dashmap = "5.4"
|
||||
envy = "0.4.2"
|
||||
futures.workspace = true
|
||||
google_ai.workspace = true
|
||||
hex.workspace = true
|
||||
http_client.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
@@ -49,7 +47,7 @@ prost.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
rpc.workspace = true
|
||||
scrypt = "0.11"
|
||||
scrypt = "0.7"
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
semantic_version.workspace = true
|
||||
semver.workspace = true
|
||||
@@ -58,12 +56,10 @@ serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
strum.workspace = true
|
||||
subtle.workspace = true
|
||||
rustc-demangle.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tokio.workspace = true
|
||||
toml.workspace = true
|
||||
@@ -83,6 +79,7 @@ channel.workspace = true
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
collab_ui = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
completion = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
@@ -91,7 +88,6 @@ fs = { workspace = true, features = ["test-support"] }
|
||||
git = { workspace = true, features = ["test-support"] }
|
||||
git_hosting_providers.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
hyper.workspace = true
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
@@ -120,6 +116,3 @@ util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
headless.workspace = true
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["async-stripe"]
|
||||
|
||||
@@ -85,11 +85,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: database
|
||||
key: url
|
||||
- name: LLM_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: llm-database
|
||||
key: url
|
||||
- name: DATABASE_MAX_CONNECTIONS
|
||||
value: "${DATABASE_MAX_CONNECTIONS}"
|
||||
- name: API_TOKEN
|
||||
@@ -97,11 +92,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: api
|
||||
key: token
|
||||
- name: LLM_API_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: llm-token
|
||||
key: secret
|
||||
- name: ZED_CLIENT_CHECKSUM_SEED
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -137,16 +127,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: google-ai
|
||||
key: api_key
|
||||
- name: QWEN2_7B_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hugging-face
|
||||
key: api_key
|
||||
- name: QWEN2_7B_API_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hugging-face
|
||||
key: qwen2_api_url
|
||||
- name: BLOB_STORE_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -12,7 +12,7 @@ metadata:
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app: nginx
|
||||
app: postgrest
|
||||
ports:
|
||||
- name: web
|
||||
protocol: TCP
|
||||
@@ -24,99 +24,17 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: nginx
|
||||
name: postgrest
|
||||
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
app: postgrest
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/nginx.conf
|
||||
subPath: nginx.conf
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: nginx-config
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: nginx-config
|
||||
data:
|
||||
nginx.conf: |
|
||||
events {}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 8080;
|
||||
|
||||
location /app/ {
|
||||
proxy_pass http://postgrest-app:8080/;
|
||||
}
|
||||
|
||||
location /llm/ {
|
||||
proxy_pass http://postgrest-llm:8080/;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: postgrest-app
|
||||
spec:
|
||||
selector:
|
||||
app: postgrest-app
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: postgrest-llm
|
||||
spec:
|
||||
selector:
|
||||
app: postgrest-llm
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: postgrest-app
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgrest-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgrest-app
|
||||
app: postgrest
|
||||
spec:
|
||||
containers:
|
||||
- name: postgrest
|
||||
@@ -137,39 +55,3 @@ spec:
|
||||
secretKeyRef:
|
||||
name: postgrest
|
||||
key: jwt_secret
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: postgrest-llm
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgrest-llm
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgrest-llm
|
||||
spec:
|
||||
containers:
|
||||
- name: postgrest
|
||||
image: "postgrest/postgrest"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: PGRST_SERVER_PORT
|
||||
value: "8080"
|
||||
- name: PGRST_DB_URI
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: llm-database
|
||||
key: url
|
||||
- name: PGRST_JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgrest
|
||||
key: jwt_secret
|
||||
|
||||
@@ -416,34 +416,3 @@ CREATE TABLE dev_server_projects (
|
||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||
paths TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billing_customers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
stripe_customer_id TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id);
|
||||
CREATE UNIQUE INDEX "uix_billing_customers_on_stripe_customer_id" ON billing_customers (stripe_customer_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billing_subscriptions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers(id),
|
||||
stripe_subscription_id TEXT NOT NULL,
|
||||
stripe_subscription_status TEXT NOT NULL,
|
||||
stripe_cancel_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);
|
||||
CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS processed_stripe_events (
|
||||
stripe_event_id TEXT PRIMARY KEY,
|
||||
stripe_event_type TEXT NOT NULL,
|
||||
stripe_event_created_timestamp INTEGER NOT NULL,
|
||||
processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS billing_subscriptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
stripe_customer_id TEXT NOT NULL,
|
||||
stripe_subscription_id TEXT NOT NULL,
|
||||
stripe_subscription_status TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "ix_billing_subscriptions_on_user_id" ON billing_subscriptions (user_id);
|
||||
CREATE INDEX "ix_billing_subscriptions_on_stripe_customer_id" ON billing_subscriptions (stripe_customer_id);
|
||||
CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id);
|
||||
@@ -1,18 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS billing_customers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
stripe_customer_id TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id);
|
||||
CREATE UNIQUE INDEX "uix_billing_customers_on_stripe_customer_id" ON billing_customers (stripe_customer_id);
|
||||
|
||||
-- Make `billing_subscriptions` reference `billing_customers` instead of having its
|
||||
-- own `user_id` and `stripe_customer_id`.
|
||||
DROP INDEX IF EXISTS "ix_billing_subscriptions_on_user_id";
|
||||
DROP INDEX IF EXISTS "ix_billing_subscriptions_on_stripe_customer_id";
|
||||
ALTER TABLE billing_subscriptions DROP COLUMN user_id;
|
||||
ALTER TABLE billing_subscriptions DROP COLUMN stripe_customer_id;
|
||||
ALTER TABLE billing_subscriptions ADD COLUMN billing_customer_id INTEGER NOT NULL REFERENCES billing_customers (id) ON DELETE CASCADE;
|
||||
CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE billing_customers ADD COLUMN last_stripe_event_id TEXT;
|
||||
ALTER TABLE billing_subscriptions ADD COLUMN last_stripe_event_id TEXT;
|
||||
@@ -1,11 +0,0 @@
|
||||
ALTER TABLE billing_customers DROP COLUMN last_stripe_event_id;
|
||||
ALTER TABLE billing_subscriptions DROP COLUMN last_stripe_event_id;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS processed_stripe_events (
|
||||
stripe_event_id TEXT PRIMARY KEY,
|
||||
stripe_event_type TEXT NOT NULL,
|
||||
stripe_event_created_timestamp BIGINT NOT NULL,
|
||||
processed_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE billing_subscriptions ADD COLUMN stripe_cancel_at TIMESTAMP WITHOUT TIME ZONE;
|
||||
@@ -1,19 +0,0 @@
|
||||
create table if not exists providers (
|
||||
id serial primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
create unique index uix_providers_on_name on providers (name);
|
||||
|
||||
create table if not exists models (
|
||||
id serial primary key,
|
||||
provider_id integer not null references providers (id) on delete cascade,
|
||||
name text not null,
|
||||
max_requests_per_minute integer not null,
|
||||
max_tokens_per_minute integer not null,
|
||||
max_tokens_per_day integer not null
|
||||
);
|
||||
|
||||
create unique index uix_models_on_provider_id_name on models (provider_id, name);
|
||||
create index ix_models_on_provider_id on models (provider_id);
|
||||
create index ix_models_on_name on models (name);
|
||||
@@ -1,19 +0,0 @@
|
||||
create table usage_measures (
|
||||
id serial primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
create unique index uix_usage_measures_on_name on usage_measures (name);
|
||||
|
||||
create table if not exists usages (
|
||||
id serial primary key,
|
||||
user_id integer not null,
|
||||
model_id integer not null references models (id) on delete cascade,
|
||||
measure_id integer not null references usage_measures (id) on delete cascade,
|
||||
timestamp timestamp without time zone not null,
|
||||
buckets bigint[] not null
|
||||
);
|
||||
|
||||
create index ix_usages_on_user_id on usages (user_id);
|
||||
create index ix_usages_on_model_id on usages (model_id);
|
||||
create unique index uix_usages_on_user_id_model_id_measure_id on usages (user_id, model_id, measure_id);
|
||||
@@ -1,4 +0,0 @@
|
||||
db-uri = "postgres://postgres@localhost/zed_llm"
|
||||
server-port = 8082
|
||||
jwt-secret = "the-postgrest-jwt-secret-for-authorization"
|
||||
log-level = "info"
|
||||
138
crates/collab/src/ai.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use rpc::proto;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub fn language_model_request_to_open_ai(
|
||||
request: proto::CompleteWithLanguageModel,
|
||||
) -> Result<open_ai::Request> {
|
||||
Ok(open_ai::Request {
|
||||
model: open_ai::Model::from_id(&request.model).unwrap_or(open_ai::Model::FourTurbo),
|
||||
messages: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|message: proto::LanguageModelRequestMessage| {
|
||||
let role = proto::LanguageModelRole::from_i32(message.role)
|
||||
.ok_or_else(|| anyhow!("invalid role {}", message.role))?;
|
||||
|
||||
let openai_message = match role {
|
||||
proto::LanguageModelRole::LanguageModelUser => open_ai::RequestMessage::User {
|
||||
content: message.content,
|
||||
},
|
||||
proto::LanguageModelRole::LanguageModelAssistant => {
|
||||
open_ai::RequestMessage::Assistant {
|
||||
content: Some(message.content),
|
||||
tool_calls: message
|
||||
.tool_calls
|
||||
.into_iter()
|
||||
.filter_map(|call| {
|
||||
Some(open_ai::ToolCall {
|
||||
id: call.id,
|
||||
content: match call.variant? {
|
||||
proto::tool_call::Variant::Function(f) => {
|
||||
open_ai::ToolCallContent::Function {
|
||||
function: open_ai::FunctionContent {
|
||||
name: f.name,
|
||||
arguments: f.arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
proto::LanguageModelRole::LanguageModelSystem => {
|
||||
open_ai::RequestMessage::System {
|
||||
content: message.content,
|
||||
}
|
||||
}
|
||||
proto::LanguageModelRole::LanguageModelTool => open_ai::RequestMessage::Tool {
|
||||
tool_call_id: message
|
||||
.tool_call_id
|
||||
.ok_or_else(|| anyhow!("tool message is missing tool call id"))?,
|
||||
content: message.content,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(openai_message)
|
||||
})
|
||||
.collect::<Result<Vec<open_ai::RequestMessage>>>()?,
|
||||
stream: true,
|
||||
stop: request.stop,
|
||||
temperature: request.temperature,
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
Some(match tool.variant? {
|
||||
proto::chat_completion_tool::Variant::Function(f) => {
|
||||
open_ai::ToolDefinition::Function {
|
||||
function: open_ai::FunctionDefinition {
|
||||
name: f.name,
|
||||
description: f.description,
|
||||
parameters: if let Some(params) = &f.parameters {
|
||||
Some(
|
||||
serde_json::from_str(params)
|
||||
.context("failed to deserialize tool parameters")
|
||||
.log_err()?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
tool_choice: request.tool_choice,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn language_model_request_to_google_ai(
|
||||
request: proto::CompleteWithLanguageModel,
|
||||
) -> Result<google_ai::GenerateContentRequest> {
|
||||
Ok(google_ai::GenerateContentRequest {
|
||||
contents: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(language_model_request_message_to_google_ai)
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
generation_config: None,
|
||||
safety_settings: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn language_model_request_message_to_google_ai(
|
||||
message: proto::LanguageModelRequestMessage,
|
||||
) -> Result<google_ai::Content> {
|
||||
let role = proto::LanguageModelRole::from_i32(message.role)
|
||||
.ok_or_else(|| anyhow!("invalid role {}", message.role))?;
|
||||
|
||||
Ok(google_ai::Content {
|
||||
parts: vec![google_ai::Part::TextPart(google_ai::TextPart {
|
||||
text: message.content,
|
||||
})],
|
||||
role: match role {
|
||||
proto::LanguageModelRole::LanguageModelUser => google_ai::Role::User,
|
||||
proto::LanguageModelRole::LanguageModelAssistant => google_ai::Role::Model,
|
||||
proto::LanguageModelRole::LanguageModelSystem => google_ai::Role::User,
|
||||
proto::LanguageModelRole::LanguageModelTool => {
|
||||
Err(anyhow!("we don't handle tool calls with google ai yet"))?
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn count_tokens_request_to_google_ai(
|
||||
request: proto::CountTokensWithLanguageModel,
|
||||
) -> Result<google_ai::CountTokensRequest> {
|
||||
Ok(google_ai::CountTokensRequest {
|
||||
contents: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(language_model_request_message_to_google_ai)
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
})
|
||||
}
|
||||