Compare commits
1 Commits
installer
...
extension-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
961e548bd6 |
6
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
@@ -23,6 +23,12 @@ body:
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
|
||||
description: Drag issues into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
|
||||
49
.github/workflows/bump_patch_version.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: bump_patch_version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Branch name to run on"
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ inputs.branch }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
bump_patch_version:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}
|
||||
|
||||
- name: Bump Patch Version
|
||||
run: |
|
||||
set -eux
|
||||
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
|
||||
tag_suffix=""
|
||||
case $channel in
|
||||
stable)
|
||||
;;
|
||||
preview)
|
||||
tag_suffix="-pre"
|
||||
;;
|
||||
*)
|
||||
echo "this must be run on either of stable|preview release branches" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit --features vendored-openssl
|
||||
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
|
||||
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
|
||||
git tag v${output}${tag_suffix}
|
||||
git push origin HEAD v${output}${tag_suffix}
|
||||
53
.github/workflows/ci.yml
vendored
@@ -54,9 +54,6 @@ jobs:
|
||||
- name: Check unused dependencies
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
|
||||
- name: Check license generation
|
||||
run: script/generate-licenses /tmp/zed_licenses_output
|
||||
|
||||
- name: Ensure fresh merge
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
@@ -173,11 +170,6 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# We need to fetch more than one commit so that `script/draft-release-notes`
|
||||
# is able to diff between the current and previous tag.
|
||||
#
|
||||
# 25 was chosen arbitrarily.
|
||||
fetch-depth: 25
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
@@ -210,9 +202,6 @@ jobs:
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p target/
|
||||
# Ignore any errors that occur while drafting release notes to not fail the build.
|
||||
script/draft-release-notes "$version" "$channel" > target/release-notes.md || true
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
@@ -256,17 +245,19 @@ jobs:
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
body_file: target/release-notes.md
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-linux:
|
||||
name: Create a Linux bundle
|
||||
bundle-deb:
|
||||
name: Create a *.deb Linux bundle
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
@@ -310,26 +301,28 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
# TODO linux : Find a way to add licenses to the final bundle
|
||||
# - name: Generate license file
|
||||
# run: script/generate-licenses
|
||||
|
||||
- name: Create and upload Linux .tar.gz bundle
|
||||
- name: Create Linux *.deb bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: zed-*.tar.gz
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.deb
|
||||
path: target/release/*.deb
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/zed-linux-x86_64.tar.gz
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# TODO linux : make it stable enough to be uploaded as a release
|
||||
# - uses: softprops/action-gh-release@v1
|
||||
# name: Upload app bundle to release
|
||||
# if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
# with:
|
||||
# draft: true
|
||||
# prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
# files: target/release/Zed.dmg
|
||||
# body: ""
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
8
.github/workflows/danger.yml
vendored
@@ -32,10 +32,4 @@ jobs:
|
||||
- name: Run Danger
|
||||
run: pnpm run --dir script/danger danger ci
|
||||
env:
|
||||
# This GitHub token is not used, but the value needs to be here to prevent
|
||||
# Danger from throwing an error.
|
||||
GITHUB_TOKEN: "not_a_real_token"
|
||||
# All requests are instead proxied through an instance of
|
||||
# https://github.com/maxdeviant/danger-proxy that allows Danger to securely
|
||||
# authenticate with GitHub while still being able to run on PRs from forks.
|
||||
DANGER_GITHUB_API_BASE_URL: "https://danger-proxy.fly.dev/github"
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
40
.github/workflows/publish_extension_cli.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Publish zed-extension CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- extension-cli
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish zed-extension CLI
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
|
||||
- name: Build extension CLI
|
||||
run: cargo build --release --package extension_cli
|
||||
|
||||
- name: Upload binary
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
run: script/upload-extension-cli ${{ github.sha }}
|
||||
11
.github/workflows/release_nightly.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
run: script/upload-nightly macos
|
||||
|
||||
bundle-deb:
|
||||
name: Create a Linux *.tar.gz bundle
|
||||
name: Create a *.deb Linux bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
needs: tests
|
||||
@@ -125,11 +125,12 @@ jobs:
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
# TODO linux : find a way to add licenses to the final bundle
|
||||
# - name: Generate license file
|
||||
# run: script/generate-licenses
|
||||
|
||||
- name: Create Linux .tar.gz bundle
|
||||
- name: Create Linux *.deb bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-targz
|
||||
run: script/upload-nightly linux-deb
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.10.5"
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/update_top_ranking_issues/requirements.txt
|
||||
- run: python script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 5393
|
||||
- run: python script/update_top_ranking_issues/main.py 5393 --github-token ${{ secrets.GITHUB_TOKEN }} --prod
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.10.5"
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/update_top_ranking_issues/requirements.txt
|
||||
- run: python script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 6952 --query-day-interval 7
|
||||
- run: python script/update_top_ranking_issues/main.py 6952 --github-token ${{ secrets.GITHUB_TOKEN }} --prod --query-day-interval 7
|
||||
|
||||
2
.gitignore
vendored
@@ -6,7 +6,7 @@
|
||||
/plugins/bin
|
||||
/script/node_modules
|
||||
/crates/theme/schemas/theme.json
|
||||
/crates/collab/seed.json
|
||||
/crates/collab/.admins.json
|
||||
/assets/*licenses.md
|
||||
**/venv
|
||||
.build
|
||||
|
||||
@@ -15,13 +15,7 @@
|
||||
"JSON": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
}
|
||||
},
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true
|
||||
"formatter": "auto"
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[
|
||||
{
|
||||
"label": "clippy",
|
||||
"command": "cargo",
|
||||
"args": ["xtask", "clippy"]
|
||||
},
|
||||
{
|
||||
"label": "assistant2",
|
||||
"command": "cargo",
|
||||
"args": ["run", "-p", "assistant2", "--example", "assistant_example"]
|
||||
}
|
||||
]
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
|
||||
|
||||
We want to avoid anyone spending time on a pull request that may not be accepted, so we suggest you discuss your ideas with the team and community before starting on major changes. Bug fixes, however, are almost always welcome.
|
||||
|
||||
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
|
||||
|
||||
## Contribution ideas
|
||||
@@ -11,7 +13,7 @@ If you're looking for ideas about what to work on, check out:
|
||||
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
|
||||
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
|
||||
|
||||
For adding themes or support for a new language to Zed, check out our [extension docs](https://github.com/zed-industries/extensions/blob/main/AUTHORING_EXTENSIONS.md).
|
||||
Outside of a handful of extremely popular languages and themes, we are generally not looking to extend Zed's language or theme support by directly building them into Zed. We really want to build a plugin system to handle making the editor extensible going forward. If you are passionate about shipping new languages or themes we suggest contributing to the extension system to help us get there faster.
|
||||
|
||||
## Proposing changes
|
||||
|
||||
|
||||
2404
Cargo.lock
generated
123
Cargo.toml
@@ -1,11 +1,9 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/anthropic",
|
||||
"crates/ai",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_tooling",
|
||||
"crates/assistant2",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -31,17 +29,13 @@ members = [
|
||||
"crates/feature_flags",
|
||||
"crates/feedback",
|
||||
"crates/file_finder",
|
||||
"crates/file_icons",
|
||||
"crates/fs",
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
"crates/git",
|
||||
"crates/go_to_line",
|
||||
"crates/google_ai",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/headless",
|
||||
"crates/image_viewer",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
@@ -57,7 +51,6 @@ members = [
|
||||
"crates/multi_buffer",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/open_ai",
|
||||
"crates/outline",
|
||||
"crates/picker",
|
||||
"crates/prettier",
|
||||
@@ -69,7 +62,6 @@ members = [
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/remote_projects",
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
@@ -77,7 +69,6 @@ members = [
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
"crates/settings",
|
||||
"crates/snippet",
|
||||
"crates/sqlez",
|
||||
@@ -85,7 +76,6 @@ members = [
|
||||
"crates/story",
|
||||
"crates/storybook",
|
||||
"crates/sum_tree",
|
||||
"crates/tab_switcher",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
"crates/text",
|
||||
@@ -95,7 +85,6 @@ members = [
|
||||
"crates/telemetry_events",
|
||||
"crates/time_format",
|
||||
"crates/ui",
|
||||
"crates/ui_text_field",
|
||||
"crates/util",
|
||||
"crates/vcs_menu",
|
||||
"crates/vim",
|
||||
@@ -105,30 +94,8 @@ members = [
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
|
||||
"extensions/astro",
|
||||
"extensions/clojure",
|
||||
"extensions/csharp",
|
||||
"extensions/dart",
|
||||
"extensions/deno",
|
||||
"extensions/elixir",
|
||||
"extensions/elm",
|
||||
"extensions/emmet",
|
||||
"extensions/erlang",
|
||||
"extensions/gleam",
|
||||
"extensions/glsl",
|
||||
"extensions/haskell",
|
||||
"extensions/html",
|
||||
"extensions/lua",
|
||||
"extensions/ocaml",
|
||||
"extensions/php",
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
"extensions/svelte",
|
||||
"extensions/terraform",
|
||||
"extensions/toml",
|
||||
"extensions/uiua",
|
||||
"extensions/vue",
|
||||
"extensions/zig",
|
||||
|
||||
"tooling/xtask",
|
||||
]
|
||||
@@ -138,11 +105,8 @@ resolver = "2"
|
||||
[workspace.dependencies]
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant2 = { path = "crates/assistant2" }
|
||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
base64 = "0.13"
|
||||
@@ -168,18 +132,14 @@ extensions_ui = { path = "crates/extensions_ui" }
|
||||
feature_flags = { path = "crates/feature_flags" }
|
||||
feedback = { path = "crates/feedback" }
|
||||
file_finder = { path = "crates/file_finder" }
|
||||
file_icons = { path = "crates/file_icons" }
|
||||
fs = { path = "crates/fs" }
|
||||
fsevent = { path = "crates/fsevent" }
|
||||
fuzzy = { path = "crates/fuzzy" }
|
||||
git = { path = "crates/git" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui" }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
headless = { path = "crates/headless" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
@@ -194,7 +154,6 @@ menu = { path = "crates/menu" }
|
||||
multi_buffer = { path = "crates/multi_buffer" }
|
||||
node_runtime = { path = "crates/node_runtime" }
|
||||
notifications = { path = "crates/notifications" }
|
||||
open_ai = { path = "crates/open_ai" }
|
||||
outline = { path = "crates/outline" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
@@ -207,7 +166,6 @@ project_symbols = { path = "crates/project_symbols" }
|
||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
remote_projects = { path = "crates/remote_projects" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
@@ -215,7 +173,6 @@ task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
settings = { path = "crates/settings" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
@@ -223,7 +180,6 @@ sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
story = { path = "crates/story" }
|
||||
storybook = { path = "crates/storybook" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
terminal = { path = "crates/terminal" }
|
||||
terminal_view = { path = "crates/terminal_view" }
|
||||
text = { path = "crates/text" }
|
||||
@@ -233,7 +189,6 @@ theme_selector = { path = "crates/theme_selector" }
|
||||
telemetry_events = { path = "crates/telemetry_events" }
|
||||
time_format = { path = "crates/time_format" }
|
||||
ui = { path = "crates/ui" }
|
||||
ui_text_field = { path = "crates/ui_text_field" }
|
||||
util = { path = "crates/util" }
|
||||
vcs_menu = { path = "crates/vcs_menu" }
|
||||
vim = { path = "crates/vim" }
|
||||
@@ -243,34 +198,29 @@ zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
anyhow = "1.0.57"
|
||||
any_vec = "0.13"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-fs = "1.6"
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
|
||||
cap-std = "3.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "61cbd6b2c224791d52b150fe535cee665cc91bb2" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "61cbd6b2c224791d52b150fe535cee665cc91bb2" }
|
||||
blade-rwh = { package = "raw-window-handle", version = "0.5" }
|
||||
cap-std = "2.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = { version = "0.11.6" }
|
||||
ctor = "0.2.6"
|
||||
ctrlc = "3.4.4"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation-sys = "0.8.6"
|
||||
derive_more = "0.99.17"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.9"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
git2 = { version = "0.18", default-features = false }
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
globset = "0.4"
|
||||
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = [
|
||||
"read-txn-no-tls",
|
||||
] }
|
||||
hex = "0.4.3"
|
||||
ignore = "0.4.22"
|
||||
indoc = "1"
|
||||
@@ -283,17 +233,13 @@ itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
nanoid = "0.4"
|
||||
nix = "0.28"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
profiling = "1"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
prost = "0.9"
|
||||
prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
prost = "0.8"
|
||||
pulldown-cmark = { version = "0.10.0", default-features = false }
|
||||
rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
@@ -321,8 +267,6 @@ tempfile = "3.9.0"
|
||||
thiserror = "1.0.29"
|
||||
tiktoken-rs = "0.5.7"
|
||||
time = { version = "0.3", features = [
|
||||
"macros",
|
||||
"parsing",
|
||||
"serde",
|
||||
"serde-well-known",
|
||||
"formatting",
|
||||
@@ -331,66 +275,82 @@ toml = "0.8"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.20", features = ["wasm"] }
|
||||
tree-sitter-astro = { git = "https://github.com/virchau13/tree-sitter-astro.git", rev = "e924787e12e8a03194f36a113290ac11d6dc10f3" }
|
||||
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
|
||||
tree-sitter-c = "0.20.1"
|
||||
tree-sitter-clojure = { git = "https://github.com/prcastro/tree-sitter-clojure", branch = "update-ts" }
|
||||
tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "dd5e59721a5f8dae34604060833902b882023aaf" }
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
|
||||
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
|
||||
tree-sitter-dockerfile = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev = "33e22c33bcdbfc33d42806ee84cfd0b1248cc392" }
|
||||
tree-sitter-dart = { git = "https://github.com/agent3bood/tree-sitter-dart", rev = "48934e3bf757a9b78f17bdfaa3e2b4284656fdc7" }
|
||||
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
|
||||
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-erlang = "0.4.0"
|
||||
tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
|
||||
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "8a99848fc734f9c4ea523b3f2a07df133cbbcec2" }
|
||||
tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
|
||||
rustc-demangle = "0.1.23"
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
|
||||
tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }
|
||||
tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" }
|
||||
tree-sitter-php = "0.21.1"
|
||||
tree-sitter-prisma-io = { git = "https://github.com/victorhqc/tree-sitter-prisma" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-purescript = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "v0.1.0" }
|
||||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }
|
||||
tree-sitter-regex = "0.20.0"
|
||||
tree-sitter-ruby = "0.20.0"
|
||||
tree-sitter-rust = "0.20.3"
|
||||
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" }
|
||||
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" }
|
||||
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
|
||||
tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
|
||||
tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
|
||||
unindent = "0.1.7"
|
||||
unicase = "2.6"
|
||||
unicode-segmentation = "1.10"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5"] }
|
||||
wasmparser = "0.201"
|
||||
wasm-encoder = "0.201"
|
||||
wasmtime = { version = "19.0.0", default-features = false, features = [
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
wasmparser = "0.121"
|
||||
wasm-encoder = "0.41"
|
||||
wasmtime = { version = "18.0", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
] }
|
||||
wasmtime-wasi = "19.0.0"
|
||||
wasmtime-wasi = "18.0"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
wit-component = "0.20"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.53.0"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_DirectWrite",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Imaging_D2D",
|
||||
"Win32_Media",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_DataExchange",
|
||||
@@ -409,7 +369,7 @@ features = [
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7b4894ba2ae81b988846676f54c0988d4027ef4f" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "4294e59279205f503eb14348dd5128bd5910c8fb" }
|
||||
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
|
||||
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" }
|
||||
|
||||
@@ -420,20 +380,15 @@ debug = "limited"
|
||||
[profile.dev.package]
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
resvg = { opt-level = 3 }
|
||||
rustybuzz = { opt-level = 3 }
|
||||
ttf-parser = { opt-level = 3 }
|
||||
wasmtime-cranelift = { opt-level = 3 }
|
||||
wasmtime = { opt-level = 3 }
|
||||
|
||||
[profile.release]
|
||||
debug = "limited"
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
[profile.release.package]
|
||||
zed = { codegen-units = 16 }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.77-bookworm as builder
|
||||
FROM rust:1.76-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
2
Procfile
@@ -1,3 +1,3 @@
|
||||
collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve
|
||||
collab: RUST_LOG=${RUST_LOG:-warn,tower_http=info,collab=info} cargo run --package=collab serve
|
||||
livekit: livekit-server --dev
|
||||
blob_store: ./script/run-local-minio
|
||||
|
||||
100
README.md
@@ -1,51 +1,49 @@
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew tap homebrew/cask-versions
|
||||
brew install zed-preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew tap homebrew/cask-versions
|
||||
brew install zed-preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
|
||||
6
assets/contexts/system.zmd
Normal file
@@ -0,0 +1,6 @@
|
||||
User input begins on a line starting with /.
|
||||
Don't apologize ever.
|
||||
Never say "I apologize".
|
||||
Use simple language and don't flatter the users.
|
||||
Keep it short.
|
||||
Risk being rude.
|
||||
@@ -1,9 +0,0 @@
|
||||
Lucide License
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -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-unfold-vertical"><path d="M12 22v-6"/><path d="M12 8V2"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/><path d="m15 19-3 3-3-3"/><path d="m15 5-3-3-3 3"/></svg>
|
||||
|
Before Width: | Height: | Size: 398 B |
@@ -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_1791_43)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8473 4.79901C13.7531 4.63587 13.6232 4.4934 13.4803 4.4109L8.51958 1.54683C8.23382 1.38181 7.76613 1.38181 7.48037 1.54683L2.51961 4.4109C2.2338 4.57587 2 4.98089 2 5.31089V11.0391C2 11.2041 2.05864 11.3879 2.15283 11.551C2.24699 11.7141 2.37691 11.8566 2.51977 11.939L7.48053 14.8031C7.7663 14.9681 8.23398 14.9681 8.51974 14.8031L13.4805 11.939C13.6234 11.8565 13.7532 11.714 13.8473 11.5509C13.9415 11.3878 14 11.204 14 11.039V5.31085C14 5.14583 13.9415 4.96211 13.8473 4.79901ZM4 8.175C4 10.3806 5.79438 12.175 7.99998 12.175C9.42327 12.175 10.7506 11.4091 11.464 10.1761L9.73295 9.17441C9.37586 9.79162 8.71182 10.175 7.99998 10.175C6.89716 10.175 5.99999 9.27778 5.99999 8.175C5.99999 7.07218 6.89716 6.17501 7.99998 6.17501C8.71174 6.17501 9.37578 6.55838 9.73284 7.17548L11.4639 6.17375C10.7505 4.9409 9.42319 4.17502 7.99998 4.17502C5.79438 4.17502 4 5.9694 4 8.175Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1791_43">
|
||||
<rect width="11.9999" height="13.5039" fill="white" transform="translate(2 1.42307)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 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_1791_60)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4803 4.4109C13.6232 4.4934 13.7531 4.63587 13.8473 4.79901C13.9415 4.96211 14 5.14583 14 5.31085V5.78958H12.8036V7.50745H11.0857V8.99892H12.8036V10.7168H14V11.039C14 11.204 13.9415 11.3878 13.8473 11.5509C13.7532 11.714 13.6234 11.8565 13.4805 11.939L8.51974 14.8031C8.23398 14.9681 7.7663 14.9681 7.48053 14.8031L2.51977 11.939C2.37691 11.8566 2.24699 11.7141 2.15283 11.551C2.05864 11.3879 2 11.2041 2 11.0391V5.31089C2 4.98089 2.2338 4.57587 2.51961 4.4109L7.48037 1.54683C7.76613 1.38181 8.23382 1.38181 8.51958 1.54683L13.4803 4.4109ZM7.99998 12.175C5.79438 12.175 4 10.3806 4 8.175C4 5.9694 5.79438 4.17502 7.99998 4.17502C9.42319 4.17502 10.7505 4.9409 11.4639 6.17375L9.73284 7.17548C9.37578 6.55838 8.71174 6.17501 7.99998 6.17501C6.89716 6.17501 5.99999 7.07218 5.99999 8.175C5.99999 9.27778 6.89716 10.175 7.99998 10.175C8.71182 10.175 9.37586 9.79162 9.73295 9.17441L11.464 10.1761C10.7506 11.4091 9.42327 12.175 7.99998 12.175Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1791_60">
|
||||
<rect width="11.9999" height="13.5039" fill="white" transform="translate(2 1.42307)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -19,11 +19,11 @@
|
||||
"bash_profile": "terminal",
|
||||
"bashrc": "terminal",
|
||||
"bmp": "image",
|
||||
"c": "c",
|
||||
"cc": "cpp",
|
||||
"cjs": "javascript",
|
||||
"c": "code",
|
||||
"cc": "code",
|
||||
"cjs": "code",
|
||||
"conf": "settings",
|
||||
"cpp": "cpp",
|
||||
"cpp": "code",
|
||||
"css": "css",
|
||||
"csv": "storage",
|
||||
"cts": "typescript",
|
||||
@@ -58,8 +58,7 @@
|
||||
"gitmodules": "vcs",
|
||||
"go": "go",
|
||||
"graphql": "graphql",
|
||||
"h": "c",
|
||||
"hpp": "cpp",
|
||||
"h": "code",
|
||||
"handlebars": "code",
|
||||
"hbs": "template",
|
||||
"heex": "elixir",
|
||||
@@ -78,8 +77,7 @@
|
||||
"jp2": "image",
|
||||
"jpeg": "image",
|
||||
"jpg": "image",
|
||||
"js": "javascript",
|
||||
"jsx": "react",
|
||||
"js": "code",
|
||||
"json": "storage",
|
||||
"jsonc": "storage",
|
||||
"jxl": "image",
|
||||
@@ -97,7 +95,7 @@
|
||||
"mdx": "document",
|
||||
"metadata": "code",
|
||||
"mkv": "video",
|
||||
"mjs": "javascript",
|
||||
"mjs": "code",
|
||||
"mka": "audio",
|
||||
"ml": "ocaml",
|
||||
"mli": "ocaml",
|
||||
@@ -154,7 +152,7 @@
|
||||
"ts": "typescript",
|
||||
"tsv": "storage",
|
||||
"ttf": "font",
|
||||
"tsx": "react",
|
||||
"tsx": "code",
|
||||
"txt": "document",
|
||||
"tcl": "tcl",
|
||||
"vue": "vue",
|
||||
@@ -163,8 +161,6 @@
|
||||
"webp": "image",
|
||||
"wma": "audio",
|
||||
"wmv": "video",
|
||||
"woff": "font",
|
||||
"woff2": "font",
|
||||
"wv": "audio",
|
||||
"xls": "document",
|
||||
"xlsx": "document",
|
||||
@@ -197,12 +193,6 @@
|
||||
"collapsed_folder": {
|
||||
"icon": "icons/file_icons/folder.svg"
|
||||
},
|
||||
"c": {
|
||||
"icon": "icons/file_icons/c.svg"
|
||||
},
|
||||
"cpp": {
|
||||
"icon": "icons/file_icons/cpp.svg"
|
||||
},
|
||||
"css": {
|
||||
"icon": "icons/file_icons/css.svg"
|
||||
},
|
||||
@@ -263,9 +253,6 @@
|
||||
"java": {
|
||||
"icon": "icons/file_icons/java.svg"
|
||||
},
|
||||
"javascript": {
|
||||
"icon": "icons/file_icons/javascript.svg"
|
||||
},
|
||||
"kotlin": {
|
||||
"icon": "icons/file_icons/kotlin.svg"
|
||||
},
|
||||
@@ -302,18 +289,15 @@
|
||||
"python": {
|
||||
"icon": "icons/file_icons/python.svg"
|
||||
},
|
||||
"r": {
|
||||
"icon": "icons/file_icons/r.svg"
|
||||
},
|
||||
"react": {
|
||||
"icon": "icons/file_icons/react.svg"
|
||||
},
|
||||
"ruby": {
|
||||
"icon": "icons/file_icons/ruby.svg"
|
||||
},
|
||||
"rust": {
|
||||
"icon": "icons/file_icons/rust.svg"
|
||||
},
|
||||
"r": {
|
||||
"icon": "icons/file_icons/r.svg"
|
||||
},
|
||||
"settings": {
|
||||
"icon": "icons/file_icons/settings.svg"
|
||||
},
|
||||
@@ -343,7 +327,7 @@
|
||||
},
|
||||
"tcl": {
|
||||
"icon": "icons/file_icons/tcl.svg"
|
||||
},
|
||||
},
|
||||
"vcs": {
|
||||
"icon": "icons/file_icons/git.svg"
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V12C2 13.1046 2.89543 14 4 14H12C13.1046 14 14 13.1046 14 12V4C14 2.89543 13.1046 2 12 2H4ZM7.26917 6.80584H6.04784V10.8808C6.04784 11.0672 6.02025 11.2241 5.96508 11.3516C5.90991 11.4791 5.82906 11.5761 5.72253 11.6427C5.61789 11.7074 5.48948 11.7397 5.33729 11.7397C5.19271 11.7397 5.06715 11.7112 4.96062 11.6541C4.85599 11.5951 4.77323 11.5114 4.71235 11.403C4.65338 11.2926 4.62199 11.1604 4.61819 11.0063H3.38829C3.38639 11.3944 3.46914 11.7169 3.63655 11.9737C3.80396 12.2286 4.03035 12.4189 4.31571 12.5444C4.60297 12.6681 4.92257 12.7299 5.27451 12.7299C5.67021 12.7299 6.0174 12.6547 6.31607 12.5045C6.61475 12.3542 6.84779 12.1402 7.0152 11.8624C7.18452 11.5847 7.26917 11.2574 7.26917 10.8808V6.80584ZM11.1672 7.95013C11.3403 8.07759 11.4383 8.25641 11.4611 8.4866H12.6453C12.6396 8.13846 12.5464 7.83218 12.3657 7.56775C12.185 7.30331 11.9319 7.0969 11.6066 6.94852C11.2832 6.80013 10.9046 6.72594 10.4709 6.72594C10.0448 6.72594 9.66429 6.80013 9.32947 6.94852C8.99464 7.0969 8.73116 7.30331 8.53902 7.56775C8.34878 7.83218 8.25461 8.14132 8.25652 8.49516C8.25461 8.92701 8.39634 9.27039 8.6817 9.52531C8.96706 9.78023 9.3561 9.96762 9.84882 10.0875L10.4852 10.2473C10.6982 10.2986 10.878 10.3557 11.0245 10.4185C11.1729 10.4813 11.2851 10.5574 11.3612 10.6468C11.4392 10.7362 11.4782 10.8465 11.4782 10.9778C11.4782 11.1186 11.4354 11.2432 11.3498 11.3516C11.2642 11.46 11.1434 11.5447 10.9874 11.6056C10.8333 11.6665 10.6516 11.6969 10.4424 11.6969C10.2293 11.6969 10.0381 11.6646 9.86879 11.5999C9.70138 11.5333 9.56727 11.4353 9.46644 11.306C9.36751 11.1747 9.31139 11.0111 9.29808 10.8151H8.10242C8.11193 11.2356 8.21371 11.5885 8.40776 11.8738C8.6037 12.1573 8.87574 12.3713 9.22388 12.5159C9.57392 12.6605 9.98484 12.7327 10.4566 12.7327C10.9322 12.7327 11.3384 12.6614 11.6751 12.5187C12.0137 12.3741 12.2725 12.1715 12.4513 11.9109C12.632 11.6484 12.7233 11.3383 12.7252 10.9806C12.7233 10.7371 12.6786 10.5212 12.5911 10.3329C12.5055 10.1445 12.3847 9.98093 12.2287 9.84206C12.0727 9.70319 11.8882 9.58619 11.6751 9.49107C11.4621 9.39595 11.2281 9.31985 10.9731 9.26278L10.4481 9.13722C10.3206 9.10869 10.2008 9.07444 10.0885 9.03449C9.97628 8.99264 9.87736 8.94413 9.79175 8.88896C9.70614 8.83189 9.63861 8.76435 9.58914 8.68635C9.54158 8.60836 9.51971 8.51704 9.52351 8.41241C9.52351 8.28685 9.55966 8.17461 9.63195 8.07569C9.70614 7.97676 9.81267 7.89971 9.95155 7.84455C10.0904 7.78747 10.2607 7.75894 10.4623 7.75894C10.7591 7.75894 10.9941 7.82267 11.1672 7.95013Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -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="M7.99752 9.14577C8.62961 9.14577 9.14201 8.63336 9.14201 8.00127C9.14201 7.36919 8.62961 6.85678 7.99752 6.85678C7.36543 6.85678 6.85303 7.36919 6.85303 8.00127C6.85303 8.63336 7.36543 9.14577 7.99752 9.14577Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37507 12.5467C5.35849 12.5371 5.26876 12.4764 5.21215 12.1996C5.15576 11.924 5.15423 11.5219 5.24075 11.0018C5.25336 10.926 5.2677 10.8485 5.28376 10.7695C5.61535 10.8289 5.96088 10.8775 6.31735 10.9146C6.52765 11.2048 6.74254 11.4797 6.9598 11.7371C6.8994 11.7906 6.83947 11.8417 6.7801 11.8906C6.37292 12.2255 6.02397 12.4252 5.75706 12.5142C5.48905 12.6036 5.39166 12.5562 5.37507 12.5467ZM4.63463 8.00002C4.48846 7.67278 4.35781 7.34921 4.24347 7.03232C4.16699 7.05793 4.09271 7.08426 4.0207 7.11126C3.52701 7.29639 3.17959 7.49875 2.96906 7.6854C2.75767 7.87282 2.75 7.98085 2.75 8C2.75 8.01916 2.75767 8.12719 2.96906 8.31461C3.17959 8.50126 3.52701 8.70361 4.0207 8.88875C4.09271 8.91575 4.167 8.94208 4.24348 8.96769C4.35782 8.65081 4.48846 8.32725 4.63463 8.00002ZM3.49402 5.70677C3.6016 5.66643 3.71247 5.6276 3.82645 5.59035C3.80173 5.47305 3.77992 5.35765 3.76108 5.24434C3.65732 4.62055 3.63633 4.01923 3.74257 3.49981C3.84858 2.98153 4.10358 2.45543 4.62507 2.15435C5.14656 1.85326 5.72968 1.89548 6.23152 2.06281C6.73448 2.23051 7.24474 2.54935 7.73308 2.95111C7.82179 3.02409 7.91084 3.10068 8.00007 3.18075C8.0893 3.10068 8.17835 3.02409 8.26706 2.9511C8.7554 2.54935 9.26566 2.23051 9.76862 2.06281C10.2705 1.89548 10.8536 1.85326 11.3751 2.15435C11.8966 2.45543 12.1516 2.98153 12.2576 3.49981C12.3638 4.01923 12.3428 4.62055 12.2391 5.24434C12.2202 5.35766 12.1984 5.47308 12.1737 5.59039C12.2876 5.62763 12.3984 5.66644 12.506 5.70677C13.0981 5.9288 13.6293 6.21129 14.026 6.56301C14.4219 6.91396 14.75 7.39783 14.75 8C14.75 8.60218 14.4219 9.08605 14.026 9.437C13.6293 9.78872 13.0981 10.0712 12.506 10.2932C12.3984 10.3336 12.2876 10.3724 12.1737 10.4096C12.1984 10.5269 12.2202 10.6424 12.2391 10.7557C12.3428 11.3795 12.3638 11.9808 12.2576 12.5002C12.1516 13.0185 11.8966 13.5446 11.3751 13.8457C10.8536 14.1468 10.2705 14.1046 9.76862 13.9372C9.26566 13.7695 8.7554 13.4507 8.26706 13.0489C8.17835 12.976 8.08931 12.8994 8.00007 12.8193C7.91084 12.8994 7.82179 12.976 7.73308 13.0489C7.24474 13.4507 6.73448 13.7695 6.23152 13.9372C5.72968 14.1046 5.14657 14.1468 4.62507 13.8457C4.10358 13.5446 3.84859 13.0185 3.74257 12.5002C3.63633 11.9808 3.65732 11.3795 3.76108 10.7557C3.77993 10.6424 3.80174 10.527 3.82646 10.4097C3.71248 10.3724 3.6016 10.3336 3.49402 10.2932C2.90192 10.0712 2.37066 9.78872 1.97395 9.437C1.57812 9.08605 1.25 8.60218 1.25 8C1.25 7.39783 1.57812 6.91396 1.97395 6.56301C2.37066 6.21129 2.90192 5.9288 3.49402 5.70677ZM9.22005 11.8906C9.16067 11.8417 9.10075 11.7906 9.04034 11.7371C9.25761 11.4797 9.4725 11.2048 9.68281 10.9145C10.0393 10.8775 10.3848 10.8289 10.7164 10.7695C10.7324 10.8485 10.7468 10.926 10.7594 11.0018C10.8459 11.5219 10.8444 11.924 10.788 12.1996C10.7314 12.4764 10.6417 12.5371 10.6251 12.5467C10.6085 12.5562 10.5111 12.6036 10.2431 12.5142C9.97617 12.4252 9.62722 12.2255 9.22005 11.8906ZM6.31737 5.08544C6.52766 4.79525 6.74254 4.52034 6.9598 4.26289C6.89939 4.20948 6.83947 4.15832 6.7801 4.10948C6.37292 3.77449 6.02397 3.57479 5.75706 3.4858C5.48905 3.39644 5.39165 3.44381 5.37507 3.45339C5.35849 3.46296 5.26876 3.52362 5.21214 3.80041C5.15576 4.07605 5.15423 4.4781 5.24075 4.99822C5.25336 5.07405 5.2677 5.15152 5.28375 5.23053C5.61535 5.1711 5.96089 5.12247 6.31737 5.08544ZM9.04034 4.26289C9.2576 4.52034 9.47249 4.79526 9.68278 5.08546C10.0393 5.12248 10.3848 5.17112 10.7164 5.23055C10.7324 5.15154 10.7468 5.07406 10.7594 4.99822C10.8459 4.4781 10.8444 4.07605 10.788 3.80041C10.7314 3.52362 10.6417 3.46296 10.6251 3.45339C10.6085 3.44381 10.5111 3.39644 10.2431 3.4858C9.97617 3.57479 9.62722 3.77449 9.22005 4.10947C9.16067 4.15832 9.10074 4.20948 9.04034 4.26289ZM11.3655 8.00002C11.5117 8.32723 11.6423 8.65078 11.7566 8.96765C11.8331 8.94205 11.9073 8.91574 11.9793 8.88875C12.473 8.70361 12.8204 8.50126 13.0309 8.31461C13.2423 8.12719 13.25 8.01916 13.25 8C13.25 7.98085 13.2423 7.87282 13.0309 7.6854C12.8204 7.49875 12.473 7.29639 11.9793 7.11126C11.9073 7.08427 11.8331 7.05796 11.7567 7.03237C11.6423 7.34924 11.5117 7.6728 11.3655 8.00002ZM7.99752 10.1458C9.18189 10.1458 10.142 9.18565 10.142 8.00127C10.142 6.8169 9.18189 5.85678 7.99752 5.85678C6.81315 5.85678 5.85303 6.8169 5.85303 8.00127C5.85303 9.18564 6.81315 10.1458 7.99752 10.1458Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-open"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></svg>
|
||||
<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-mail-open"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 390 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m12 6.668 2-2L11.332 2l-2 2M12 6.668l-6.668 6.664H2.668v-2.664L9.332 4M12 6.668 9.332 4" stroke="black" stroke-width="1" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 239 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 0.875C5.49797 0.875 3.875 2.49797 3.875 4.5C3.875 6.15288 4.98124 7.54738 6.49373 7.98351C5.2997 8.12901 4.27557 8.55134 3.50407 9.31167C2.52216 10.2794 2.02502 11.72 2.02502 13.5999C2.02502 13.8623 2.23769 14.0749 2.50002 14.0749C2.76236 14.0749 2.97502 13.8623 2.97502 13.5999C2.97502 11.8799 3.42786 10.7206 4.17091 9.9883C4.91536 9.25463 6.02674 8.87499 7.49995 8.87499C8.97317 8.87499 10.0846 9.25463 10.8291 9.98831C11.5721 10.7206 12.025 11.8799 12.025 13.5999C12.025 13.8623 12.2376 14.0749 12.5 14.0749C12.7623 14.075 12.975 13.8623 12.975 13.6C12.975 11.72 12.4778 10.2794 11.4959 9.31166C10.7244 8.55135 9.70025 8.12903 8.50625 7.98352C10.0187 7.5474 11.125 6.15289 11.125 4.5C11.125 2.49797 9.50203 0.875 7.5 0.875ZM4.825 4.5C4.825 3.02264 6.02264 1.825 7.5 1.825C8.97736 1.825 10.175 3.02264 10.175 4.5C10.175 5.97736 8.97736 7.175 7.5 7.175C6.02264 7.175 4.825 5.97736 4.825 4.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -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-git-pull-request-arrow"><circle cx="5" cy="6" r="3"/><path d="M5 9v12"/><circle cx="19" cy="18" r="3"/><path d="m15 9-3-3 3-3"/><path d="M12 6h5a2 2 0 0 1 2 2v7"/></svg>
|
||||
|
Before Width: | Height: | Size: 372 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="4" cy="11" r="1" fill="#787D87"/>
|
||||
<path d="M9 2.5V5M9 5V7.5M9 5H11.5M9 5H6.5M9 5L10.6667 3.33333M9 5L7.33333 6.6667M9 5L10.6667 6.6667M9 5L7.33333 3.33333" stroke="#787D87" stroke-width="1.25" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 333 B |
4
assets/icons/reply_arrow_left.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 17V15.8C20 14.1198 20 13.2798 19.673 12.638C19.3854 12.0735 18.9265 11.6146 18.362 11.327C17.7202 11 16.8802 11 15.2 11H4M4 11L8 7M4 11L8 15" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 468 B |
@@ -1,3 +1,56 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.668 11.332v-.797c0-1.12 0-1.683.219-2.11.191-.374.496-.683.87-.874.43-.219.99-.219 2.11-.219h7.469m0 0-2.668-2.664m2.668 2.664L10.668 10" stroke="black" stroke-width="1.33334" stroke-linecap="round"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="reply-svgrepo-com.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.39996789"
|
||||
inkscape:cx="435.03492"
|
||||
inkscape:cy="417.53351"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="847"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<g
|
||||
id="SVGRepo_bgCarrier"
|
||||
stroke-width="0" />
|
||||
<g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<g
|
||||
id="SVGRepo_iconCarrier"
|
||||
transform="matrix(-1,0,0,1,24.001548,0)">
|
||||
<path
|
||||
d="M 20,17 V 15.8 C 20,14.1198 20,13.2798 19.673,12.638 19.3854,12.0735 18.9265,11.6146 18.362,11.327 17.7202,11 16.8802,11 15.2,11 H 4 m 0,0 4,-4 m -4,4 4,4"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 296 B After Width: | Height: | Size: 1.7 KiB |
@@ -1,5 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.5 7V9.5M9.5 9.5V12M9.5 9.5H12M9.5 9.5H7M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#687076" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M2.19368 3.84945C2.1919 4.2642 2.32866 4.59866 2.60675 4.84709C2.88054 5.09168 3.25138 5.26935 3.71611 5.38242L4.30921 5.53136C4.50605 5.57882 4.67126 5.63135 4.80449 5.68845C4.93818 5.74501 5.03566 5.81208 5.10003 5.88772C5.16394 5.96098 5.19718 6.05224 5.19718 6.16391C5.19718 6.28414 5.16092 6.38935 5.08822 6.48143C5.01498 6.5742 4.91033 6.6484 4.77143 6.70261C4.63494 6.75654 4.47187 6.78432 4.28148 6.78432C4.08803 6.78432 3.91609 6.75498 3.76493 6.69728C3.61656 6.63822 3.49926 6.55211 3.41134 6.43944C3.35022 6.35823 3.30749 6.26206 3.28377 6.14994C3.2624 6.0489 3.17722 5.96227 3.0652 5.96227H2.26368C2.14684 5.96227 2.04844 6.05921 2.05922 6.18014C2.08844 6.50804 2.18262 6.79104 2.34333 7.02737C2.53198 7.30027 2.79379 7.50589 3.12635 7.64401C3.46002 7.78184 3.84995 7.85002 4.29478 7.85002C4.74295 7.85002 5.12861 7.78282 5.45025 7.64653C5.77317 7.50864 6.02261 7.31419 6.19553 7.06219C6.37039 6.80819 6.45792 6.50903 6.45974 6.16684C6.45792 5.93398 6.41515 5.72484 6.33014 5.5418C6.24742 5.35989 6.13063 5.20177 5.98009 5.06775C5.8304 4.9345 5.65391 4.82275 5.45112 4.73222C5.24921 4.64208 5.02797 4.57018 4.78759 4.51634L4.29843 4.39937C4.18153 4.37319 4.07118 4.3417 3.96872 4.30525C3.86717 4.26736 3.77849 4.22377 3.70234 4.17473C3.62798 4.1251 3.57039 4.06719 3.52851 4.00126C3.49014 3.93817 3.47159 3.86314 3.47483 3.77409L3.47486 3.77227C3.47486 3.66565 3.50529 3.57148 3.56614 3.4881C3.62872 3.40477 3.71979 3.33803 3.84237 3.28933C3.96413 3.2393 4.11652 3.21306 4.3001 3.21306C4.57008 3.21306 4.77748 3.27107 4.92756 3.38156C5.04237 3.4661 5.1184 3.57596 5.15675 3.71261C5.18354 3.80804 5.26638 3.89144 5.37613 3.89144H6.17261C6.28854 3.89144 6.38808 3.79532 6.37517 3.67384C6.34688 3.40772 6.26053 3.16833 6.11582 2.9566C5.94161 2.70171 5.69822 2.5037 5.38764 2.36203L5.36689 2.40752M2.19368 3.84945C2.19189 3.51006 2.28244 3.21141 2.46646 2.95562C2.65136 2.70115 2.90449 2.50328 3.2237 2.36181C3.54318 2.22022 3.90496 2.15002 4.30809 2.15002C4.71811 2.15002 5.07832 2.22011 5.38764 2.36203L5.36689 2.40752M4.7896 6.74919C4.93504 6.69243 5.04766 6.61351 5.12747 6.51242ZM4.7896 6.74919L5.12747 6.51242ZM5.12747 6.51242C5.20728 6.41132 5.24718 6.29516 5.24718 6.16391ZM5.12747 6.51242L5.24718 6.16391ZM5.24718 6.16391C5.24718 6.04154 5.21082 5.93867 5.13811 5.85531L5.24718 6.16391Z" fill="#687076"/>
|
||||
<path d="M2.19368 3.84945C2.1919 4.2642 2.32866 4.59866 2.60675 4.84709C2.88054 5.09168 3.25138 5.26935 3.71611 5.38242L4.30921 5.53136C4.50605 5.57882 4.67126 5.63135 4.80449 5.68845C4.93818 5.74501 5.03566 5.81208 5.10003 5.88772C5.16394 5.96098 5.19718 6.05224 5.19718 6.16391C5.19718 6.28414 5.16092 6.38935 5.08822 6.48143C5.01498 6.5742 4.91033 6.6484 4.77143 6.70261C4.63494 6.75654 4.47187 6.78432 4.28148 6.78432C4.08803 6.78432 3.91609 6.75498 3.76493 6.69728C3.61656 6.63822 3.49926 6.55211 3.41134 6.43944C3.35022 6.35823 3.30749 6.26206 3.28377 6.14994C3.2624 6.0489 3.17722 5.96227 3.0652 5.96227H2.26368C2.14684 5.96227 2.04844 6.05921 2.05922 6.18014C2.08844 6.50804 2.18262 6.79104 2.34333 7.02737C2.53198 7.30027 2.79379 7.50589 3.12635 7.64401C3.46002 7.78184 3.84995 7.85002 4.29478 7.85002C4.74295 7.85002 5.12861 7.78282 5.45025 7.64653C5.77317 7.50864 6.02261 7.31419 6.19553 7.06219C6.37039 6.80819 6.45792 6.50903 6.45974 6.16684C6.45792 5.93398 6.41515 5.72484 6.33014 5.5418C6.24742 5.35989 6.13063 5.20177 5.98009 5.06775C5.8304 4.9345 5.65391 4.82275 5.45112 4.73222C5.24921 4.64208 5.02797 4.57018 4.78759 4.51634L4.29843 4.39937C4.18153 4.37319 4.07118 4.3417 3.96872 4.30525C3.86717 4.26736 3.77849 4.22377 3.70234 4.17473C3.62798 4.1251 3.57039 4.06719 3.52851 4.00126C3.49014 3.93817 3.47159 3.86314 3.47483 3.77409L3.47486 3.77227C3.47486 3.66565 3.50529 3.57148 3.56614 3.4881C3.62872 3.40477 3.71979 3.33803 3.84237 3.28933C3.96413 3.2393 4.11652 3.21306 4.3001 3.21306C4.57008 3.21306 4.77748 3.27107 4.92756 3.38156C5.04237 3.4661 5.1184 3.57596 5.15675 3.71261C5.18354 3.80804 5.26638 3.89144 5.37613 3.89144H6.17261C6.28854 3.89144 6.38808 3.79532 6.37517 3.67384C6.34688 3.40772 6.26053 3.16833 6.11582 2.9566C5.94161 2.70171 5.69822 2.5037 5.38764 2.36203M2.19368 3.84945C2.19189 3.51006 2.28244 3.21141 2.46646 2.95562C2.65136 2.70115 2.90449 2.50328 3.2237 2.36181C3.54318 2.22022 3.90496 2.15002 4.30809 2.15002C4.71811 2.15002 5.07832 2.22011 5.38764 2.36203M2.19368 3.84945L2.24368 3.84942M5.38764 2.36203L5.36689 2.40752M5.36689 2.40752C5.06539 2.26919 4.71246 2.20002 4.30809 2.20002C3.91081 2.20002 3.5561 2.26919 3.24396 2.40752C2.93181 2.54586 2.68618 2.73829 2.50705 2.98482M5.36689 2.40752C5.67017 2.54586 5.90605 2.73829 6.07454 2.98482C6.21435 3.18938 6.29799 3.42082 6.32545 3.67912C6.3349 3.76801 6.262 3.84144 6.17261 3.84144H5.37613C5.29388 3.84144 5.22712 3.77829 5.20489 3.6991C5.16352 3.55168 5.08096 3.43241 4.9572 3.3413C4.79581 3.22247 4.57678 3.16306 4.3001 3.16306M4.7896 6.74919C4.64595 6.80594 4.47657 6.83432 4.28148 6.83432C4.08285 6.83432 3.9046 6.80417 3.74676 6.74386C3.59069 6.68179 3.46565 6.59045 3.37165 6.46985M4.7896 6.74919C4.93504 6.69243 5.04766 6.61351 5.12747 6.51242M4.7896 6.74919L5.12747 6.51242M5.12747 6.51242C5.20728 6.41132 5.24718 6.29516 5.24718 6.16391M5.12747 6.51242L5.24718 6.16391M5.24718 6.16391C5.24718 6.04154 5.21082 5.93867 5.13811 5.85531L5.24718 6.16391Z" stroke="#687076" stroke-width="0.1"/>
|
||||
<path d="M9.5 7V9.5M9.5 12V9.5M12 9.5H9.5M7 9.5H9.5M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
|
||||
<path d="M2.19366 3.84943C2.19188 4.26418 2.32864 4.59864 2.60673 4.84707C2.88052 5.09166 3.25136 5.26933 3.71609 5.3824C3.71616 5.38242 3.71623 5.38243 3.7163 5.38245L4.30919 5.53134L4.30919 5.53134L4.30965 5.53145C4.50649 5.57891 4.67124 5.63133 4.80447 5.68843L4.80469 5.68852C4.93838 5.74508 5.03564 5.81206 5.10001 5.8877L5.10001 5.8877L5.10041 5.88816C5.16432 5.96142 5.19716 6.05222 5.19716 6.16389C5.19716 6.28412 5.1609 6.38933 5.0882 6.48141C5.01496 6.57418 4.91031 6.64838 4.77141 6.70259L4.77121 6.70266C4.63472 6.75659 4.47185 6.7843 4.28146 6.7843C4.08801 6.7843 3.91607 6.75496 3.76491 6.69726C3.61654 6.6382 3.49924 6.55209 3.41132 6.43942C3.3502 6.35821 3.30747 6.26204 3.28375 6.14992C3.26238 6.04888 3.1772 5.96225 3.06518 5.96225H2.26366C2.14682 5.96225 2.04842 6.05919 2.0592 6.18012C2.08842 6.50802 2.1826 6.79102 2.34331 7.02735L2.34352 7.02767C2.53217 7.30057 2.79377 7.50587 3.12633 7.64399L3.12642 7.64402C3.46009 7.78185 3.84993 7.85 4.29476 7.85C4.74293 7.85 5.12859 7.7828 5.45023 7.64651L5.45036 7.64646C5.77328 7.50857 6.02259 7.31417 6.19551 7.06217C6.37037 6.80817 6.4579 6.50901 6.45972 6.16682L6.45972 6.16616C6.4579 5.9333 6.41513 5.72482 6.33012 5.54178C6.2474 5.35987 6.13061 5.20175 5.98007 5.06773C5.83038 4.93448 5.65389 4.82273 5.4511 4.7322C5.24919 4.64206 5.02795 4.57016 4.78757 4.51632L4.29841 4.39935L4.29841 4.39934L4.29771 4.39919C4.18081 4.37301 4.07116 4.34168 3.9687 4.30523C3.86715 4.26734 3.77847 4.22375 3.70232 4.17471C3.62796 4.12508 3.57037 4.06717 3.52849 4.00124C3.49012 3.93815 3.47157 3.86312 3.47481 3.77407L3.47484 3.77407V3.77225C3.47484 3.66563 3.50527 3.57146 3.56612 3.48808C3.6287 3.40475 3.71977 3.33801 3.84235 3.28931L3.84235 3.28932L3.84289 3.28909C3.96465 3.23906 4.1165 3.21304 4.30008 3.21304C4.57006 3.21304 4.77746 3.27105 4.92754 3.38154C5.04235 3.46608 5.11838 3.57594 5.15673 3.71259C5.18352 3.80802 5.26636 3.89142 5.37611 3.89142H6.17259C6.28852 3.89142 6.38806 3.7953 6.37515 3.67382C6.34686 3.4077 6.26051 3.16831 6.1158 2.95658C5.94159 2.70169 5.6982 2.50368 5.38762 2.36201L5.36687 2.4075M2.19366 3.84943C2.19187 3.51004 2.28242 3.21139 2.46644 2.9556L2.46658 2.9554C2.65148 2.70093 2.90447 2.50326 3.22368 2.36179C3.54316 2.2202 3.90494 2.15 4.30807 2.15C4.71809 2.15 5.07841 2.22014 5.38773 2.36206L5.36687 2.4075M2.19366 3.84943C2.19366 3.84951 2.19366 3.84959 2.19366 3.84967L2.24366 3.8494L2.19366 3.84918C2.19366 3.84926 2.19366 3.84935 2.19366 3.84943ZM5.36687 2.4075C5.06537 2.26917 4.71244 2.2 4.30807 2.2C3.91079 2.2 3.55608 2.26917 3.24394 2.4075C2.93179 2.54584 2.68616 2.73827 2.50703 2.9848L3.82389 3.24285L3.82389 3.24285C3.95336 3.18964 4.11209 3.16304 4.30008 3.16304C4.57676 3.16304 4.79579 3.22245 4.95718 3.34128C5.08094 3.43239 5.1635 3.55166 5.20487 3.69908C5.2271 3.77827 5.29386 3.84142 5.37611 3.84142H6.17259C6.26198 3.84142 6.33488 3.76799 6.32543 3.6791C6.29797 3.4208 6.21433 3.18936 6.07452 2.9848C5.90603 2.73827 5.67015 2.54584 5.36687 2.4075ZM4.78958 6.74917C4.64593 6.80592 4.47655 6.8343 4.28146 6.8343C4.08283 6.8343 3.90458 6.80415 3.74674 6.74384C3.59067 6.68177 3.46563 6.59043 3.37163 6.46983L4.78958 6.74917ZM4.78958 6.74917C4.93502 6.69241 5.04764 6.61349 5.12745 6.5124M4.78958 6.74917L5.12745 6.5124M5.12745 6.5124C5.20726 6.4113 5.24716 6.29514 5.24716 6.16389M5.12745 6.5124L5.24716 6.16389M5.24716 6.16389C5.24716 6.04152 5.2108 5.93865 5.13809 5.85529L5.24716 6.16389Z" fill="#687076" stroke="#687076" stroke-width="0.1"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -1,16 +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"
|
||||
>
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 413 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36667 3.79167C5.53364 3.79167 4.85833 4.46697 4.85833 5.3C4.85833 6.13303 5.53364 6.80833 6.36667 6.80833C7.1997 6.80833 7.875 6.13303 7.875 5.3C7.875 4.46697 7.1997 3.79167 6.36667 3.79167ZM2.1 5.925H3.67944C3.9626 7.14732 5.05824 8.05833 6.36667 8.05833C7.67509 8.05833 8.77073 7.14732 9.05389 5.925H14.9C15.2452 5.925 15.525 5.64518 15.525 5.3C15.525 4.95482 15.2452 4.675 14.9 4.675H9.05389C8.77073 3.45268 7.67509 2.54167 6.36667 2.54167C5.05824 2.54167 3.9626 3.45268 3.67944 4.675H2.1C1.75482 4.675 1.475 4.95482 1.475 5.3C1.475 5.64518 1.75482 5.925 2.1 5.925ZM13.3206 12.325C13.0374 13.5473 11.9418 14.4583 10.6333 14.4583C9.32491 14.4583 8.22927 13.5473 7.94611 12.325H2.1C1.75482 12.325 1.475 12.0452 1.475 11.7C1.475 11.3548 1.75482 11.075 2.1 11.075H7.94611C8.22927 9.85268 9.32491 8.94167 10.6333 8.94167C11.9418 8.94167 13.0374 9.85268 13.3206 11.075H14.9C15.2452 11.075 15.525 11.3548 15.525 11.7C15.525 12.0452 15.2452 12.325 14.9 12.325H13.3206ZM9.125 11.7C9.125 10.867 9.8003 10.1917 10.6333 10.1917C11.4664 10.1917 12.1417 10.867 12.1417 11.7C12.1417 12.533 11.4664 13.2083 10.6333 13.2083C9.8003 13.2083 9.125 12.533 9.125 11.7Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -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-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||
|
Before Width: | Height: | Size: 410 B |
@@ -3,3 +3,4 @@
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.64907 9.32382C8.313 9.13287 8.08213 8.81954 7.94725 8.4078C7.8147 8.00318 7.75317 7.44207 7.75317 6.73677C7.75317 6.03845 7.81141 5.48454 7.9369 5.08716L7.93755 5.08512C8.07231 4.67373 8.3034 4.36258 8.64088 4.17794C8.96806 3.99257 9.41119 3.9104 9.9496 3.9104C10.3406 3.9104 10.6632 3.95585 10.8967 4.06485C11.0079 4.11675 11.1099 4.18844 11.2033 4.27745V2.03027H12.4077V9.4856H11.2033V9.18983C11.0945 9.29074 10.98 9.37096 10.8591 9.42752C10.6327 9.53648 10.3335 9.58252 9.97867 9.58252C9.4339 9.58252 8.98592 9.50355 8.65375 9.3264L8.64907 9.32382ZM11.1139 7.85508C11.1841 7.60311 11.2227 7.23354 11.2227 6.73677C11.2227 6.24602 11.1841 5.88331 11.1141 5.63844C11.0457 5.39902 10.9401 5.25863 10.8149 5.18266L10.8077 5.17826C10.6804 5.09342 10.4713 5.03726 10.1531 5.03726C9.80785 5.03726 9.5719 5.09359 9.42256 5.1832L9.41829 5.18576C9.28002 5.26412 9.16722 5.40602 9.09399 5.64263C9.01876 5.88566 8.97694 6.24668 8.97694 6.73677C8.97694 7.23363 9.01882 7.59774 9.09399 7.8406C9.1673 8.07745 9.28097 8.22477 9.42256 8.30972C9.5719 8.39933 9.80785 8.45566 10.1531 8.45566C10.4721 8.45566 10.683 8.40265 10.8114 8.32216C10.9396 8.23944 11.0456 8.09373 11.1139 7.85508Z" fill="#787D87"/>
|
||||
<rect x="1.14087" y="10.7188" width="11.7183" height="1.26565" rx="0.632824" fill="#787D87"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,6 +1,4 @@
|
||||
[
|
||||
// todo(linux): Review the editor bindings
|
||||
// Standard Linux bindings
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
@@ -11,14 +9,14 @@
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"ctrl-up": "menu::SelectFirst",
|
||||
"ctrl-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"shift-f10": "menu::ShowContextMenu",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"shift-enter": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"shift-enter": "menu::UseSelectedQuery",
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"ctrl-o": "workspace::Open",
|
||||
@@ -28,7 +26,9 @@
|
||||
"ctrl-0": "zed::ResetBufferFontSize",
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"alt-f9": "zed::Hide",
|
||||
"ctrl-h": "zed::Hide",
|
||||
"alt-ctrl-h": "zed::HideOthers",
|
||||
"ctrl-m": "zed::Minimize",
|
||||
"f11": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
@@ -38,107 +38,87 @@
|
||||
"escape": "editor::Cancel",
|
||||
"backspace": "editor::Backspace",
|
||||
"shift-backspace": "editor::Backspace",
|
||||
"ctrl-h": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
// "ctrl-backspace": "editor::DeleteToBeginningOfLine",
|
||||
// "ctrl-delete": "editor::DeleteToEndOfLine",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
// "ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
// "alt-h": "editor::DeleteToPreviousWordStart",
|
||||
// "alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-delete": "editor::DeleteToEndOfLine",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-h": "editor::DeleteToPreviousWordStart",
|
||||
"alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
// "ctrl-up": "editor::MoveToStartOfParagraph", todo(linux) Should be "scroll down by 1 line"
|
||||
"ctrl-up": "editor::MoveToStartOfParagraph",
|
||||
"pageup": "editor::PageUp",
|
||||
// "shift-pageup": "editor::MovePageUp", todo(linux) should be 'select page up'
|
||||
"shift-pageup": "editor::MovePageUp",
|
||||
"home": "editor::MoveToBeginningOfLine",
|
||||
"down": "editor::MoveDown",
|
||||
// "ctrl-down": "editor::MoveToEndOfParagraph", todo(linux) should be "scroll up by 1 line"
|
||||
"ctrl-down": "editor::MoveToEndOfParagraph",
|
||||
"pagedown": "editor::PageDown",
|
||||
// "shift-pagedown": "editor::MovePageDown", todo(linux) should be 'select page down'
|
||||
"shift-pagedown": "editor::MovePageDown",
|
||||
"end": "editor::MoveToEndOfLine",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight",
|
||||
"ctrl-left": "editor::MoveToPreviousWordStart",
|
||||
// "alt-b": "editor::MoveToPreviousWordStart",
|
||||
"ctrl-right": "editor::MoveToNextWordEnd",
|
||||
// "alt-f": "editor::MoveToNextWordEnd",
|
||||
// "cmd-left": "editor::MoveToBeginningOfLine",
|
||||
// "ctrl-a": "editor::MoveToBeginningOfLine",
|
||||
// "cmd-right": "editor::MoveToEndOfLine",
|
||||
// "ctrl-e": "editor::MoveToEndOfLine",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-shift-l": "editor::NextScreen", // todo(linux): What is this
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
"alt-f": "editor::MoveToNextWordEnd",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"ctrl-=end": "editor::MoveToEnd",
|
||||
"shift-up": "editor::SelectUp",
|
||||
"shift-down": "editor::SelectDown",
|
||||
"ctrl-shift-n": "editor::SelectDown",
|
||||
"shift-left": "editor::SelectLeft",
|
||||
"ctrl-shift-b": "editor::SelectLeft",
|
||||
"shift-right": "editor::SelectRight",
|
||||
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
// "ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
// "ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
"ctrl-shift-f": "editor::SelectRight",
|
||||
"alt-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd",
|
||||
"alt-shift-f": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"ctrl-l": "editor::SelectLine",
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
// "cmd-shift-left": [
|
||||
// "editor::SelectToBeginningOfLine",
|
||||
// {
|
||||
// "stop_at_soft_wraps": true
|
||||
// }
|
||||
// ],
|
||||
"shift-home": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
// "ctrl-shift-a": [
|
||||
// "editor::SelectToBeginningOfLine",
|
||||
// {
|
||||
// "stop_at_soft_wraps": true
|
||||
// }
|
||||
// ],
|
||||
// "cmd-shift-right": [
|
||||
// "editor::SelectToEndOfLine",
|
||||
// {
|
||||
// "stop_at_soft_wraps": true
|
||||
// }
|
||||
// ],
|
||||
"shift-end": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
// "ctrl-shift-e": [
|
||||
// "editor::SelectToEndOfLine",
|
||||
// {
|
||||
// "stop_at_soft_wraps": true
|
||||
// }
|
||||
// ],
|
||||
// "alt-v": [
|
||||
// "editor::MovePageUp",
|
||||
// {
|
||||
// "center_cursor": true
|
||||
// }
|
||||
// ],
|
||||
"ctrl-alt-space": "editor::ShowCharacterPalette",
|
||||
"ctrl-shift-e": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
|
||||
"ctrl-alt-g b": "editor::ToggleGitBlame"
|
||||
"ctrl-alt-z": "editor::RevertSelectedHunks"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -146,37 +126,30 @@
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"ctrl-shift-enter": "editor::NewlineBelow",
|
||||
"ctrl-enter": "editor::NewlineAbove",
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": [
|
||||
"ctrl-f": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"replace_enabled": true
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
// "cmd-e": [
|
||||
// "buffer_search::Deploy",
|
||||
// {
|
||||
// "focus": false
|
||||
// }
|
||||
// ],
|
||||
"ctrl->": "assistant::QuoteSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && inline_completion",
|
||||
"context": "Editor && mode == full && copilot_suggestion",
|
||||
"bindings": {
|
||||
"alt-]": "editor::NextInlineCompletion",
|
||||
"alt-[": "editor::PreviousInlineCompletion",
|
||||
"alt-right": "editor::AcceptPartialInlineCompletion"
|
||||
"alt-]": "copilot::NextSuggestion",
|
||||
"alt-[": "copilot::PreviousSuggestion",
|
||||
"alt-right": "editor::AcceptPartialCopilotSuggestion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion",
|
||||
"context": "Editor && !copilot_suggestion",
|
||||
"bindings": {
|
||||
"alt-\\": "editor::ShowInlineCompletion"
|
||||
"alt-\\": "copilot::Suggest"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -190,8 +163,8 @@
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPrevMatch"
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -212,8 +185,7 @@
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace"
|
||||
"alt-tab": "search::CycleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -234,10 +206,11 @@
|
||||
"context": "ProjectSearchBar",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"ctrl-shift-f": "search::FocusSearch",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-ctrl-g": "search::ToggleRegex",
|
||||
"alt-ctrl-x": "search::ToggleRegex"
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -251,39 +224,44 @@
|
||||
"context": "ProjectSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"ctrl-alt-enter": "search::ReplaceAll"
|
||||
"ctrl-enter": "search::ReplaceAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchView",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-ctrl-g": "search::ToggleRegex",
|
||||
"alt-ctrl-x": "search::ToggleRegex"
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-{": "pane::ActivatePrevItem",
|
||||
"ctrl-}": "pane::ActivateNextItem",
|
||||
"ctrl-alt-left": "pane::ActivatePrevItem",
|
||||
"ctrl-alt-right": "pane::ActivateNextItem",
|
||||
"ctrl-w": "pane::CloseActiveItem",
|
||||
"alt-ctrl-t": "pane::CloseInactiveItems",
|
||||
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-alt-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-shift-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-k u": "pane::CloseCleanItems",
|
||||
"ctrl-k w": "pane::CloseAllItems",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPrevMatch",
|
||||
"ctrl-alt-shift-h": "search::ToggleReplace",
|
||||
"ctrl-k ctrl-w": "pane::CloseAllItems",
|
||||
"ctrl-f": "project_search::ToggleFocus",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
"alt-r": "search::ToggleRegex",
|
||||
"alt-ctrl-f": "project_search::ToggleFilters",
|
||||
"ctrl-alt-shift-r": "search::ToggleRegex",
|
||||
"ctrl-alt-shift-x": "search::ToggleRegex"
|
||||
"ctrl-alt-c": "search::ToggleCaseSensitive",
|
||||
"ctrl-alt-w": "search::ToggleWholeWord",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-alt-f": "project_search::ToggleFilters",
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
@@ -292,17 +270,8 @@
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"shift-alt-up": "editor::AddSelectionAbove",
|
||||
"shift-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
|
||||
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding
|
||||
"ctrl-shift-down": "editor::SelectSmallerSyntaxNode", //todo(linux) tmp keybinding
|
||||
"ctrl-alt-up": "editor::AddSelectionAbove",
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
@@ -335,6 +304,8 @@
|
||||
"advance_downwards": false
|
||||
}
|
||||
],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
@@ -343,16 +314,15 @@
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"ctrl-f12": "editor::GoToTypeDefinition",
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-shift-[": "editor::Fold",
|
||||
"ctrl-shift-]": "editor::UnfoldLines",
|
||||
"ctrl-alt-[": "editor::Fold",
|
||||
"ctrl-alt-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"alt-ctrl-r": "editor::RevealInFinder",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames"
|
||||
"ctrl-alt-r": "editor::RevealInFinder",
|
||||
"ctrl-alt-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -365,18 +335,18 @@
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"alt-1": ["pane::ActivateItem", 0],
|
||||
"alt-2": ["pane::ActivateItem", 1],
|
||||
"alt-3": ["pane::ActivateItem", 2],
|
||||
"alt-4": ["pane::ActivateItem", 3],
|
||||
"alt-5": ["pane::ActivateItem", 4],
|
||||
"alt-6": ["pane::ActivateItem", 5],
|
||||
"alt-7": ["pane::ActivateItem", 6],
|
||||
"alt-8": ["pane::ActivateItem", 7],
|
||||
"alt-9": ["pane::ActivateItem", 8],
|
||||
"alt-0": "pane::ActivateLastItem",
|
||||
"ctrl-alt--": "pane::GoBack",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"ctrl-1": ["pane::ActivateItem", 0],
|
||||
"ctrl-2": ["pane::ActivateItem", 1],
|
||||
"ctrl-3": ["pane::ActivateItem", 2],
|
||||
"ctrl-4": ["pane::ActivateItem", 3],
|
||||
"ctrl-5": ["pane::ActivateItem", 4],
|
||||
"ctrl-6": ["pane::ActivateItem", 5],
|
||||
"ctrl-7": ["pane::ActivateItem", 6],
|
||||
"ctrl-8": ["pane::ActivateItem", 7],
|
||||
"ctrl-9": ["pane::ActivateItem", 8],
|
||||
"ctrl-0": "pane::ActivateLastItem",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"ctrl-_": "pane::GoForward",
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus"
|
||||
}
|
||||
@@ -391,8 +361,8 @@
|
||||
// "create_new_window": true
|
||||
// }
|
||||
// ]
|
||||
"alt-ctrl-o": "projects::OpenRecent",
|
||||
"alt-ctrl-shift-b": "branches::OpenRecent",
|
||||
"ctrl-alt-o": "projects::OpenRecent",
|
||||
"ctrl-alt-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-k s": "workspace::SaveWithoutFormat",
|
||||
@@ -400,33 +370,24 @@
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
"alt-3": ["workspace::ActivatePane", 2],
|
||||
"alt-4": ["workspace::ActivatePane", 3],
|
||||
"alt-5": ["workspace::ActivatePane", 4],
|
||||
"alt-6": ["workspace::ActivatePane", 5],
|
||||
"alt-7": ["workspace::ActivatePane", 6],
|
||||
"alt-8": ["workspace::ActivatePane", 7],
|
||||
"alt-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-alt-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-b": "workspace::ToggleRightDock",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
"ctrl-3": ["workspace::ActivatePane", 2],
|
||||
"ctrl-4": ["workspace::ActivatePane", 3],
|
||||
"ctrl-5": ["workspace::ActivatePane", 4],
|
||||
"ctrl-6": ["workspace::ActivatePane", 5],
|
||||
"ctrl-7": ["workspace::ActivatePane", 6],
|
||||
"ctrl-8": ["workspace::ActivatePane", 7],
|
||||
"ctrl-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-r": "workspace::ToggleRightDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": [
|
||||
"pane::DeploySearch",
|
||||
{
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymap",
|
||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||
"ctrl-shift-t": "project_symbols::Toggle",
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymap",
|
||||
"ctrl-t": "project_symbols::Toggle",
|
||||
"ctrl-p": "file_finder::Toggle",
|
||||
"ctrl-tab": "tab_switcher::Toggle",
|
||||
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
|
||||
"ctrl-e": "file_finder::Toggle",
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
@@ -447,12 +408,15 @@
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
// todo(linux) make sure these match linux bindings or remove above comment?
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-d": "editor::DuplicateLine",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-alt-up": "editor::MoveLineUp",
|
||||
"ctrl-alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
@@ -468,6 +432,7 @@
|
||||
}
|
||||
},
|
||||
// Bindings from Atom
|
||||
// todo(linux) make sure these match linux bindings or remove above comment?
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
@@ -513,7 +478,7 @@
|
||||
"bindings": {
|
||||
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
||||
// TODO: Move this to a dock open action
|
||||
"ctrl-shift-c": "collab_panel::ToggleFocus",
|
||||
"ctrl-alt-c": "collab_panel::ToggleFocus",
|
||||
"ctrl-alt-i": "zed::DebugElements",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
@@ -522,7 +487,6 @@
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
"ctrl-k enter": "editor::OpenExcerptsSplit",
|
||||
"ctrl-f8": "editor::GoToHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPrevHunk",
|
||||
@@ -541,19 +505,19 @@
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-n": "project_panel::NewFile",
|
||||
"alt-ctrl-n": "project_panel::NewDirectory",
|
||||
"ctrl-alt-n": "project_panel::NewDirectory",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"ctrl-alt-c": "project_panel::CopyPath",
|
||||
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
|
||||
"ctrl-alt-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Trash",
|
||||
"delete": "project_panel::Trash",
|
||||
"backspace": "project_panel::Delete",
|
||||
"delete": "project_panel::Delete",
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": true }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": true }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFinder",
|
||||
"ctrl-alt-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
}
|
||||
},
|
||||
@@ -589,36 +553,34 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder",
|
||||
"bindings": { "ctrl-shift-p": "file_finder::SelectPrev" }
|
||||
},
|
||||
{
|
||||
"context": "TabSwitcher",
|
||||
"context": "ChatPanel > MessageEditor",
|
||||
"bindings": {
|
||||
"ctrl-up": "menu::SelectPrev",
|
||||
"ctrl-down": "menu::SelectNext",
|
||||
"ctrl-shift-tab": "menu::SelectPrev",
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
"escape": "chat_panel::CloseReplyPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"shift-ctrl-c": "terminal::Copy",
|
||||
"shift-ctrl-v": "terminal::Paste",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-k": "terminal::Clear",
|
||||
// Some nice conveniences
|
||||
"ctrl-backspace": ["terminal::SendText", "\u0015"],
|
||||
"ctrl-right": ["terminal::SendText", "\u0005"],
|
||||
"ctrl-left": ["terminal::SendText", "\u0001"],
|
||||
// Terminal.app compatibility
|
||||
"alt-left": ["terminal::SendText", "\u001bb"],
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||
"down": ["terminal::SendKeystroke", "down"],
|
||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
|
||||
// Some nice conveniences
|
||||
"ctrl-backspace": ["terminal::SendText", "\u0015"],
|
||||
"ctrl-right": ["terminal::SendText", "\u0005"],
|
||||
"ctrl-left": ["terminal::SendText", "\u0001"]
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -13,15 +13,11 @@
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-enter": "menu::ShowContextMenu",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"shift-enter": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"shift-enter": "menu::UseSelectedQuery",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
@@ -158,8 +154,7 @@
|
||||
],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-alt-z": "editor::RevertSelectedHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame"
|
||||
"cmd-alt-z": "editor::RevertSelectedHunks"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -170,11 +165,10 @@
|
||||
"cmd-shift-enter": "editor::NewlineAbove",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"cmd-f": "buffer_search::Deploy",
|
||||
"cmd-alt-f": [
|
||||
"cmd-f": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"replace_enabled": true
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
"cmd-e": [
|
||||
@@ -187,17 +181,17 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && inline_completion",
|
||||
"context": "Editor && mode == full && copilot_suggestion",
|
||||
"bindings": {
|
||||
"alt-]": "editor::NextInlineCompletion",
|
||||
"alt-[": "editor::PreviousInlineCompletion",
|
||||
"alt-right": "editor::AcceptPartialInlineCompletion"
|
||||
"alt-]": "copilot::NextSuggestion",
|
||||
"alt-[": "copilot::PreviousSuggestion",
|
||||
"alt-right": "editor::AcceptPartialCopilotSuggestion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion",
|
||||
"context": "Editor && !copilot_suggestion",
|
||||
"bindings": {
|
||||
"alt-\\": "editor::ShowInlineCompletion"
|
||||
"alt-\\": "copilot::Suggest"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -209,15 +203,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantChat > Editor", // Used in the assistant2 crate
|
||||
"bindings": {
|
||||
"enter": ["assistant2::Submit", "Simple"],
|
||||
"cmd-enter": ["assistant2::Submit", "Codebase"],
|
||||
"escape": "assistant2::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel", // Used in the assistant crate, which we're replacing
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch"
|
||||
@@ -241,8 +227,7 @@
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "search::ToggleReplace"
|
||||
"alt-tab": "search::CycleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -263,10 +248,11 @@
|
||||
"context": "ProjectSearchBar",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"cmd-shift-f": "search::FocusSearch",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
"alt-cmd-x": "search::ToggleRegex"
|
||||
"alt-cmd-g": "search::ActivateRegexMode",
|
||||
"alt-cmd-s": "search::ActivateSemanticMode",
|
||||
"alt-cmd-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -287,9 +273,11 @@
|
||||
"context": "ProjectSearchView",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
"alt-cmd-x": "search::ToggleRegex"
|
||||
"alt-cmd-g": "search::ActivateRegexMode",
|
||||
"alt-cmd-s": "search::ActivateSemanticMode",
|
||||
"alt-cmd-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -311,9 +299,11 @@
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"alt-cmd-f": "project_search::ToggleFilters",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
"alt-cmd-x": "search::ToggleRegex"
|
||||
"alt-cmd-g": "search::ActivateRegexMode",
|
||||
"alt-cmd-s": "search::ActivateSemanticMode",
|
||||
"alt-cmd-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
@@ -329,8 +319,13 @@
|
||||
"cmd-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"alt-shift-up": "editor::DuplicateLineUp",
|
||||
"alt-shift-down": "editor::DuplicateLineDown",
|
||||
"alt-shift-up": [
|
||||
"editor::DuplicateLine",
|
||||
{
|
||||
"move_upwards": true
|
||||
}
|
||||
],
|
||||
"alt-shift-down": "editor::DuplicateLine",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode",
|
||||
"cmd-d": [
|
||||
@@ -373,7 +368,6 @@
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"cmd-f12": "editor::GoToTypeDefinition",
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
@@ -444,18 +438,10 @@
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"alt-cmd-y": "workspace::CloseAllDocks",
|
||||
"cmd-shift-f": "pane::DeploySearch",
|
||||
"cmd-shift-h": [
|
||||
"pane::DeploySearch",
|
||||
{
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"cmd-t": "project_symbols::Toggle",
|
||||
"cmd-p": "file_finder::Toggle",
|
||||
"ctrl-tab": "tab_switcher::Toggle",
|
||||
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
|
||||
"cmd-shift-p": "command_palette::Toggle",
|
||||
"cmd-shift-m": "diagnostics::Deploy",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
@@ -549,7 +535,6 @@
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
"cmd-k enter": "editor::OpenExcerptsSplit",
|
||||
"cmd-f8": "editor::GoToHunk",
|
||||
"cmd-shift-f8": "editor::GoToPrevHunk",
|
||||
@@ -576,8 +561,8 @@
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Trash",
|
||||
"delete": "project_panel::Trash",
|
||||
"backspace": "project_panel::Delete",
|
||||
"delete": "project_panel::Delete",
|
||||
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": true }],
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
@@ -616,16 +601,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder",
|
||||
"bindings": { "cmd-shift-p": "file_finder::SelectPrev" }
|
||||
},
|
||||
{
|
||||
"context": "TabSwitcher",
|
||||
"context": "ChatPanel > MessageEditor",
|
||||
"bindings": {
|
||||
"ctrl-up": "menu::SelectPrev",
|
||||
"ctrl-down": "menu::SelectNext",
|
||||
"ctrl-shift-tab": "menu::SelectPrev",
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
"escape": "chat_panel::CloseReplyPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"cmd-d": "editor::DuplicateLineDown",
|
||||
"cmd-d": "editor::DuplicateLine",
|
||||
"cmd-backspace": "editor::DeleteLine",
|
||||
"cmd-pagedown": "editor::MovePageDown",
|
||||
"cmd-pageup": "editor::MovePageUp",
|
||||
|
||||
@@ -13,15 +13,11 @@
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-enter": "menu::ShowContextMenu",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-q": "storybook::Quit",
|
||||
"backspace": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight"
|
||||
"cmd-q": "storybook::Quit"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"cmd-l": "go_to_line::Toggle",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-d": "editor::DuplicateLine",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-j": "editor::ScrollCursorCenter",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
|
||||
@@ -73,17 +73,8 @@
|
||||
],
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
|
||||
"/": "vim::Search",
|
||||
"?": [
|
||||
"vim::Search",
|
||||
{
|
||||
"backwards": true
|
||||
}
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"n": "vim::MoveToNextMatch",
|
||||
"shift-n": "vim::MoveToPrevMatch",
|
||||
"n": "search::SelectNextMatch",
|
||||
"shift-n": "search::SelectPrevMatch",
|
||||
"%": "vim::Matching",
|
||||
"f": [
|
||||
"vim::PushOperator",
|
||||
@@ -146,10 +137,8 @@
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToTypeDefinition",
|
||||
"g x": "editor::OpenUrl",
|
||||
"g n": "vim::SelectNextMatch",
|
||||
"g shift-n": "vim::SelectPreviousMatch",
|
||||
"g l": "vim::SelectNext",
|
||||
"g shift-l": "vim::SelectPrevious",
|
||||
"g n": "vim::SelectNext",
|
||||
"g shift-n": "vim::SelectPrevious",
|
||||
"g >": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
@@ -234,8 +223,6 @@
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
"shift-h": "vim::WindowTop",
|
||||
"shift-m": "vim::WindowMiddle",
|
||||
"shift-l": "vim::WindowBottom",
|
||||
@@ -362,6 +349,15 @@
|
||||
],
|
||||
"u": "editor::Undo",
|
||||
"ctrl-r": "editor::Redo",
|
||||
"/": "vim::Search",
|
||||
"?": [
|
||||
"vim::Search",
|
||||
{
|
||||
"backwards": true
|
||||
}
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
@@ -369,15 +365,6 @@
|
||||
"< <": "vim::Outdent",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
@@ -395,52 +382,18 @@
|
||||
"d": "editor::Rename" // zed specific
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == c",
|
||||
"bindings": {
|
||||
"s": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"ChangeSurrounds": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == d",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == y",
|
||||
"bindings": {
|
||||
"s": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"AddSurrounds": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == ys",
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
@@ -549,18 +502,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal",
|
||||
"bindings": {
|
||||
"g c c": "editor::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual",
|
||||
"bindings": {
|
||||
"g c": "editor::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
@@ -569,7 +510,7 @@
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"ctrl-x ctrl-o": "editor::ShowCompletions",
|
||||
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
|
||||
"ctrl-x ctrl-c": "editor::ShowInlineCompletion", // zed specific
|
||||
"ctrl-x ctrl-c": "copilot::Suggest", // zed specific
|
||||
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
|
||||
"ctrl-x ctrl-z": "editor::Cancel",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
@@ -605,12 +546,6 @@
|
||||
"escape": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen",
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
// netrw compatibility
|
||||
"context": "ProjectPanel && not_editing",
|
||||
@@ -619,22 +554,18 @@
|
||||
"%": "project_panel::NewFile",
|
||||
"/": "project_panel::NewSearchInDirectory",
|
||||
"d": "project_panel::NewDirectory",
|
||||
"enter": "project_panel::OpenPermanent",
|
||||
"enter": "project_panel::Open",
|
||||
"escape": "project_panel::ToggleFocus",
|
||||
"h": "project_panel::CollapseSelectedEntry",
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrev",
|
||||
"l": "project_panel::ExpandSelectedEntry",
|
||||
"o": "project_panel::OpenPermanent",
|
||||
"o": "project_panel::Open",
|
||||
"shift-d": "project_panel::Delete",
|
||||
"shift-r": "project_panel::Rename",
|
||||
"t": "project_panel::OpenPermanent",
|
||||
"v": "project_panel::OpenPermanent",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFinder",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent"
|
||||
"t": "project_panel::Open",
|
||||
"v": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFinder"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
// },
|
||||
"buffer_line_height": "comfortable",
|
||||
// The name of a font to use for rendering text in the UI
|
||||
"ui_font_family": ".SystemUIFont",
|
||||
"ui_font_family": "Zed Sans",
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
// Disable ligatures:
|
||||
@@ -47,30 +47,16 @@
|
||||
// The factor to grow the active pane by. Defaults to 1.0
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
// Centered layout related settings.
|
||||
"centered_layout": {
|
||||
// The relative width of the left padding of the central pane from the
|
||||
// workspace when the centered layout is used.
|
||||
"left_padding": 0.2,
|
||||
// The relative width of the right padding of the central pane from the
|
||||
// workspace when the centered layout is used.
|
||||
"right_padding": 0.2
|
||||
},
|
||||
// The key to use for adding multiple cursors
|
||||
// Currently "alt" or "cmd_or_ctrl" (also aliased as
|
||||
// "cmd" and "ctrl") are supported.
|
||||
// Currently "alt" or "cmd" are supported.
|
||||
"multi_cursor_modifier": "alt",
|
||||
// Whether to enable vim modes and key bindings.
|
||||
// Whether to enable vim modes and key bindings
|
||||
"vim_mode": false,
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Whether to confirm before quitting Zed.
|
||||
"confirm_quit": false,
|
||||
// Whether to restore last closed project when fresh Zed instance is opened.
|
||||
"restore_on_startup": "last_workspace",
|
||||
// Size of the drop target in the editor.
|
||||
"drop_target_size": 0.2,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
@@ -83,7 +69,7 @@
|
||||
// documentation when not included in original completion list.
|
||||
"completion_documentation_secondary_query_debounce": 300,
|
||||
// Whether to show wrap guides in the editor. Setting this to true will
|
||||
// show a guide at the 'preferred_line_length' value if 'soft_wrap' is set to
|
||||
// show a guide at the 'preferred_line_length' value if softwrap is set to
|
||||
// 'preferred_line_length', and will show any additional guides as specified
|
||||
// by the 'wrap_guides' setting.
|
||||
"show_wrap_guides": true,
|
||||
@@ -103,16 +89,9 @@
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
// Whether to automatically add matching closing characters when typing
|
||||
// opening parenthesis, bracket, brace, single or double quote characters.
|
||||
// For example, when you type (, Zed will add a closing ) at the correct position.
|
||||
// Whether to automatically type closing characters for you. For example,
|
||||
// when you type (, Zed will automatically add a closing ) at the correct position.
|
||||
"use_autoclose": true,
|
||||
// Controls how the editor handles the autoclosed characters.
|
||||
// When set to `false`(default), skipping over and auto-removing of the closing characters
|
||||
// happen only for auto-inserted characters.
|
||||
// Otherwise(when `true`), the closing characters are always skipped over and auto-removed
|
||||
// no matter how they were inserted.
|
||||
"always_treat_brackets_as_autoclosed": false,
|
||||
// Controls whether copilot provides suggestion immediately
|
||||
// or waits for a `copilot::Toggle`
|
||||
"show_copilot_suggestions": true,
|
||||
@@ -155,14 +134,12 @@
|
||||
// 4. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": "auto",
|
||||
// Whether to show cursor positions in the scrollbar.
|
||||
"cursors": true,
|
||||
// Whether to show git diff indicators in the scrollbar.
|
||||
"git_diff": true,
|
||||
// Whether to show buffer search results in the scrollbar.
|
||||
"search_results": true,
|
||||
// Whether to show selected symbol occurrences in the scrollbar.
|
||||
"selected_symbol": true,
|
||||
// Whether to show selections in the scrollbar.
|
||||
"selections": true,
|
||||
// Whether to show symbols selections in the scrollbar.
|
||||
"symbols_selections": true,
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
},
|
||||
@@ -185,9 +162,6 @@
|
||||
},
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
"relative_line_numbers": false,
|
||||
// When to populate a new search's query based on the text under the cursor.
|
||||
// This setting can take the following three values:
|
||||
@@ -216,8 +190,6 @@
|
||||
"scroll_debounce_ms": 50
|
||||
},
|
||||
"project_panel": {
|
||||
// Whether to show the project panel button in the status bar
|
||||
"button": true,
|
||||
// Default width of the project panel.
|
||||
"default_width": 240,
|
||||
// Where to dock the project panel. Can be 'left' or 'right'.
|
||||
@@ -233,10 +205,7 @@
|
||||
// Whether to reveal it in the project panel automatically,
|
||||
// when a corresponding project entry becomes active.
|
||||
// Gitignored entries are never auto revealed.
|
||||
"auto_reveal_entries": true,
|
||||
/// Whether to fold directories automatically
|
||||
/// when a directory has only one directory inside.
|
||||
"auto_fold_dirs": false
|
||||
"auto_reveal_entries": true
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
@@ -268,10 +237,6 @@
|
||||
"default_width": 380
|
||||
},
|
||||
"assistant": {
|
||||
// Version of this setting.
|
||||
"version": "1",
|
||||
// Whether the assistant is enabled.
|
||||
"enabled": true,
|
||||
// Whether to show the assistant panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'.
|
||||
@@ -280,26 +245,34 @@
|
||||
"default_width": 640,
|
||||
// Default height when the assistant is docked to the bottom.
|
||||
"default_height": 320,
|
||||
// AI provider.
|
||||
// Deprecated: Please use `provider.api_url` instead.
|
||||
// The default OpenAI API endpoint to use when starting new conversations.
|
||||
"openai_api_url": "https://api.openai.com/v1",
|
||||
// Deprecated: Please use `provider.default_model` instead.
|
||||
// The default OpenAI model to use when starting new conversations. This
|
||||
// setting can take three values:
|
||||
//
|
||||
// 1. "gpt-3.5-turbo-0613""
|
||||
// 2. "gpt-4-0613""
|
||||
// 3. "gpt-4-1106-preview"
|
||||
"default_open_ai_model": "gpt-4-1106-preview",
|
||||
"provider": {
|
||||
"name": "openai",
|
||||
// The default model to use when starting new conversations. This
|
||||
"type": "openai",
|
||||
// The default OpenAI API endpoint to use when starting new conversations.
|
||||
"api_url": "https://api.openai.com/v1",
|
||||
// The default OpenAI model to use when starting new conversations. This
|
||||
// setting can take three values:
|
||||
//
|
||||
// 1. "gpt-3.5-turbo"
|
||||
// 2. "gpt-4"
|
||||
// 3. "gpt-4-turbo-preview"
|
||||
"default_model": "gpt-4-turbo-preview"
|
||||
// 1. "gpt-3.5-turbo-0613""
|
||||
// 2. "gpt-4-0613""
|
||||
// 3. "gpt-4-1106-preview"
|
||||
"default_model": "gpt-4-1106-preview"
|
||||
}
|
||||
},
|
||||
// Whether the screen sharing icon is shown in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
"enable_language_server": true,
|
||||
// The list of language servers to use (or disable) for all languages.
|
||||
//
|
||||
// This is typically customized on a per-language basis.
|
||||
"language_servers": ["..."],
|
||||
// When to automatically save edited buffers. This setting can
|
||||
// take four values.
|
||||
//
|
||||
@@ -312,11 +285,6 @@
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Settings related to the editor's tab bar.
|
||||
"tab_bar": {
|
||||
// Whether or not to show the navigation history buttons.
|
||||
"show_nav_history_buttons": true
|
||||
},
|
||||
// Settings related to the editor's tabs
|
||||
"tabs": {
|
||||
// Show git status colors in the editor tabs.
|
||||
@@ -324,18 +292,6 @@
|
||||
// Position of the close button on the editor tabs.
|
||||
"close_position": "right"
|
||||
},
|
||||
// Settings related to preview tabs.
|
||||
"preview_tabs": {
|
||||
// Whether preview tabs should be enabled.
|
||||
// Preview tabs allow you to open files in preview mode, where they close automatically
|
||||
// when you switch to another file unless you explicitly pin them.
|
||||
// This is useful for quickly viewing files without cluttering your workspace.
|
||||
"enabled": true,
|
||||
// Whether to open tabs in preview mode when selected from the file finder.
|
||||
"enable_preview_from_file_finder": false,
|
||||
// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
|
||||
"enable_preview_from_code_navigation": false
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
@@ -415,15 +371,7 @@
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files",
|
||||
// Control whether the git blame information is shown inline,
|
||||
// in the currently focused line.
|
||||
"inline_blame": {
|
||||
"enabled": true
|
||||
// Sets a delay after which the inline blame information is shown.
|
||||
// Delay is restarted with every cursor movement.
|
||||
// "delay_ms": 600
|
||||
}
|
||||
"git_gutter": "tracked_files"
|
||||
},
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
@@ -512,8 +460,6 @@
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Whether to show the terminal button in the status bar
|
||||
"button": true,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// environment. Use `:` to separate multiple values.
|
||||
"env": {
|
||||
@@ -559,6 +505,35 @@
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
// "max_scroll_history_lines": 10000,
|
||||
},
|
||||
// Difference settings for semantic_index
|
||||
"semantic_index": {
|
||||
"enabled": true
|
||||
},
|
||||
// Settings specific to our elixir integration
|
||||
"elixir": {
|
||||
// Change the LSP zed uses for elixir.
|
||||
// Note that changing this setting requires a restart of Zed
|
||||
// to take effect.
|
||||
//
|
||||
// May take 3 values:
|
||||
// 1. Use the standard ElixirLS, this is the default
|
||||
// "lsp": "elixir_ls"
|
||||
// 2. Use the experimental NextLs
|
||||
// "lsp": "next_ls",
|
||||
// 3. Use a language server installed locally on your machine:
|
||||
// "lsp": {
|
||||
// "local": {
|
||||
// "path": "~/next-ls/bin/start",
|
||||
// "arguments": ["--stdio"]
|
||||
// }
|
||||
// },
|
||||
//
|
||||
"lsp": "elixir_ls"
|
||||
},
|
||||
// Settings specific to our deno integration
|
||||
"deno": {
|
||||
"enable": false
|
||||
},
|
||||
"code_actions_on_format": {},
|
||||
// An object whose keys are language names, and whose values
|
||||
// are arrays of filenames or extensions of files that should
|
||||
@@ -573,38 +548,55 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
//
|
||||
// If you don't want any of these extensions, add this field to your settings
|
||||
// and change the value to `false`.
|
||||
"auto_install_extensions": {
|
||||
"html": true
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"C++": {
|
||||
"format_on_save": "off"
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"C": {
|
||||
"format_on_save": "off"
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true,
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"Make": {
|
||||
"hard_tabs": true
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"Prisma": {
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Terraform": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"YAML": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"JSON": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"OCaml": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"OCaml Interface": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
|
||||
// project has no other Prettier installed.
|
||||
"prettier": {
|
||||
// Use regular Prettier json configuration:
|
||||
@@ -653,17 +645,5 @@
|
||||
// Mostly useful for developers who are managing multiple instances of Zed.
|
||||
"dev": {
|
||||
// "theme": "Andromeda"
|
||||
},
|
||||
// Task-related settings.
|
||||
"task": {
|
||||
// Whether to show task status indicator in the status bar. Default: true
|
||||
"show_status_indicator": true
|
||||
},
|
||||
// Whether to show full labels in line indicator or short ones
|
||||
//
|
||||
// Values:
|
||||
// - `short`: "2 s, 15 l, 32 c"
|
||||
// - `long`: "2 selections, 15 lines, 32 characters"
|
||||
// Default: long
|
||||
"line_indicator_format": "long"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#618399ff",
|
||||
"hint.background": "#12231fff",
|
||||
"hint.border": "#183934ff",
|
||||
"ignored": "#6b6b73ff",
|
||||
"ignored": "#aca8aeff",
|
||||
"ignored.background": "#262933ff",
|
||||
"ignored.border": "#2b2f38ff",
|
||||
"info": "#10a793ff",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#706897ff",
|
||||
"hint.background": "#161a35ff",
|
||||
"hint.border": "#222953ff",
|
||||
"ignored": "#756f7eff",
|
||||
"ignored": "#898591ff",
|
||||
"ignored.background": "#3a353fff",
|
||||
"ignored.border": "#56505eff",
|
||||
"info": "#566ddaff",
|
||||
@@ -495,7 +495,7 @@
|
||||
"hint": "#776d9dff",
|
||||
"hint.background": "#e1e0f9ff",
|
||||
"hint.border": "#c8c7f2ff",
|
||||
"ignored": "#6e6876ff",
|
||||
"ignored": "#5a5462ff",
|
||||
"ignored.background": "#bfbcc5ff",
|
||||
"ignored.border": "#8f8b96ff",
|
||||
"info": "#586cdaff",
|
||||
@@ -879,7 +879,7 @@
|
||||
"hint": "#b17272ff",
|
||||
"hint.background": "#171e38ff",
|
||||
"hint.border": "#262f56ff",
|
||||
"ignored": "#8f8b77ff",
|
||||
"ignored": "#a4a08bff",
|
||||
"ignored.background": "#45433bff",
|
||||
"ignored.border": "#6c695cff",
|
||||
"info": "#6684e0ff",
|
||||
@@ -1263,7 +1263,7 @@
|
||||
"hint": "#b37979ff",
|
||||
"hint.background": "#e3e5faff",
|
||||
"hint.border": "#cdd1f5ff",
|
||||
"ignored": "#878471ff",
|
||||
"ignored": "#706d5fff",
|
||||
"ignored.background": "#cecab4ff",
|
||||
"ignored.border": "#a8a48eff",
|
||||
"info": "#6684dfff",
|
||||
@@ -1647,7 +1647,7 @@
|
||||
"hint": "#6f815aff",
|
||||
"hint.background": "#142319ff",
|
||||
"hint.border": "#1c3927ff",
|
||||
"ignored": "#7d7c6aff",
|
||||
"ignored": "#91907fff",
|
||||
"ignored.background": "#424136ff",
|
||||
"ignored.border": "#5d5c4cff",
|
||||
"info": "#36a165ff",
|
||||
@@ -2031,7 +2031,7 @@
|
||||
"hint": "#758961ff",
|
||||
"hint.background": "#d9ecdfff",
|
||||
"hint.border": "#bbddc6ff",
|
||||
"ignored": "#767463ff",
|
||||
"ignored": "#61604fff",
|
||||
"ignored.background": "#c5c4b9ff",
|
||||
"ignored.border": "#969585ff",
|
||||
"info": "#37a165ff",
|
||||
@@ -2415,7 +2415,7 @@
|
||||
"hint": "#a77087ff",
|
||||
"hint.background": "#0f1c3dff",
|
||||
"hint.border": "#182d5bff",
|
||||
"ignored": "#8e8683ff",
|
||||
"ignored": "#a79f9dff",
|
||||
"ignored.background": "#443c39ff",
|
||||
"ignored.border": "#665f5cff",
|
||||
"info": "#407ee6ff",
|
||||
@@ -2799,7 +2799,7 @@
|
||||
"hint": "#a67287ff",
|
||||
"hint.background": "#dfe3fbff",
|
||||
"hint.border": "#c6cef7ff",
|
||||
"ignored": "#837b78ff",
|
||||
"ignored": "#6a6360ff",
|
||||
"ignored.background": "#ccc7c5ff",
|
||||
"ignored.border": "#aaa3a1ff",
|
||||
"info": "#407ee6ff",
|
||||
@@ -3183,7 +3183,7 @@
|
||||
"hint": "#8d70a8ff",
|
||||
"hint.background": "#0d1a43ff",
|
||||
"hint.border": "#192961ff",
|
||||
"ignored": "#908190ff",
|
||||
"ignored": "#a899a8ff",
|
||||
"ignored.background": "#433a43ff",
|
||||
"ignored.border": "#675b67ff",
|
||||
"info": "#5169ebff",
|
||||
@@ -3567,7 +3567,7 @@
|
||||
"hint": "#8c70a6ff",
|
||||
"hint.background": "#e2dffcff",
|
||||
"hint.border": "#cac7faff",
|
||||
"ignored": "#857785ff",
|
||||
"ignored": "#6b5e6bff",
|
||||
"ignored.background": "#c6b8c6ff",
|
||||
"ignored.border": "#ad9dadff",
|
||||
"info": "#5169ebff",
|
||||
@@ -3951,7 +3951,7 @@
|
||||
"hint": "#52809aff",
|
||||
"hint.background": "#121c24ff",
|
||||
"hint.border": "#1a2f3cff",
|
||||
"ignored": "#688c9dff",
|
||||
"ignored": "#7c9fb3ff",
|
||||
"ignored.background": "#33444dff",
|
||||
"ignored.border": "#4f6a78ff",
|
||||
"info": "#267eadff",
|
||||
@@ -4335,7 +4335,7 @@
|
||||
"hint": "#5a87a0ff",
|
||||
"hint.background": "#d8e4eeff",
|
||||
"hint.border": "#b9cee0ff",
|
||||
"ignored": "#628496ff",
|
||||
"ignored": "#526f7dff",
|
||||
"ignored.background": "#a6cadcff",
|
||||
"ignored.border": "#80a4b6ff",
|
||||
"info": "#267eadff",
|
||||
@@ -4719,7 +4719,7 @@
|
||||
"hint": "#8a647aff",
|
||||
"hint.background": "#1c1b29ff",
|
||||
"hint.border": "#2c2b45ff",
|
||||
"ignored": "#756e6eff",
|
||||
"ignored": "#898383ff",
|
||||
"ignored.background": "#3b3535ff",
|
||||
"ignored.border": "#564e4eff",
|
||||
"info": "#7272caff",
|
||||
@@ -5103,7 +5103,7 @@
|
||||
"hint": "#91697fff",
|
||||
"hint.background": "#e4e1f5ff",
|
||||
"hint.border": "#cecaecff",
|
||||
"ignored": "#6e6666ff",
|
||||
"ignored": "#5a5252ff",
|
||||
"ignored.background": "#c1bbbbff",
|
||||
"ignored.border": "#8e8989ff",
|
||||
"info": "#7272caff",
|
||||
@@ -5487,7 +5487,7 @@
|
||||
"hint": "#607e76ff",
|
||||
"hint.background": "#151e20ff",
|
||||
"hint.border": "#1f3233ff",
|
||||
"ignored": "#6f7e74ff",
|
||||
"ignored": "#859188ff",
|
||||
"ignored.background": "#353f39ff",
|
||||
"ignored.border": "#505e55ff",
|
||||
"info": "#468b8fff",
|
||||
@@ -5871,7 +5871,7 @@
|
||||
"hint": "#66847cff",
|
||||
"hint.background": "#dae7e8ff",
|
||||
"hint.border": "#bed4d6ff",
|
||||
"ignored": "#68766dff",
|
||||
"ignored": "#546259ff",
|
||||
"ignored.background": "#bcc5bfff",
|
||||
"ignored.border": "#8b968eff",
|
||||
"info": "#488b90ff",
|
||||
@@ -6255,7 +6255,7 @@
|
||||
"hint": "#008b9fff",
|
||||
"hint.background": "#051949ff",
|
||||
"hint.border": "#102667ff",
|
||||
"ignored": "#778f77ff",
|
||||
"ignored": "#8ba48bff",
|
||||
"ignored.background": "#3b453bff",
|
||||
"ignored.border": "#5c6c5cff",
|
||||
"info": "#3e62f4ff",
|
||||
@@ -6639,7 +6639,7 @@
|
||||
"hint": "#008fa1ff",
|
||||
"hint.background": "#e1ddfeff",
|
||||
"hint.border": "#c9c4fdff",
|
||||
"ignored": "#718771ff",
|
||||
"ignored": "#5f705fff",
|
||||
"ignored.background": "#b4ceb4ff",
|
||||
"ignored.border": "#8ea88eff",
|
||||
"info": "#3e61f4ff",
|
||||
@@ -7023,7 +7023,7 @@
|
||||
"hint": "#6c81a5ff",
|
||||
"hint.background": "#161f2bff",
|
||||
"hint.border": "#203348ff",
|
||||
"ignored": "#7e849eff",
|
||||
"ignored": "#959bb2ff",
|
||||
"ignored.background": "#3e4769ff",
|
||||
"ignored.border": "#5b6385ff",
|
||||
"info": "#3e8ed0ff",
|
||||
@@ -7407,7 +7407,7 @@
|
||||
"hint": "#7087b2ff",
|
||||
"hint.background": "#dde7f6ff",
|
||||
"hint.border": "#c2d5efff",
|
||||
"ignored": "#767d9aff",
|
||||
"ignored": "#5f6789ff",
|
||||
"ignored.background": "#c1c5d8ff",
|
||||
"ignored.border": "#9a9fb6ff",
|
||||
"info": "#3e8fd0ff",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#628b80ff",
|
||||
"hint.background": "#0d2f4eff",
|
||||
"hint.border": "#1b4a6eff",
|
||||
"ignored": "#696a6aff",
|
||||
"ignored": "#8a8986ff",
|
||||
"ignored.background": "#313337ff",
|
||||
"ignored.border": "#3f4043ff",
|
||||
"info": "#5ac1feff",
|
||||
@@ -480,7 +480,7 @@
|
||||
"hint": "#8ca7c2ff",
|
||||
"hint.background": "#deebfaff",
|
||||
"hint.border": "#c4daf6ff",
|
||||
"ignored": "#a9acaeff",
|
||||
"ignored": "#8b8e92ff",
|
||||
"ignored.background": "#dcdddeff",
|
||||
"ignored.border": "#cfd1d2ff",
|
||||
"info": "#3b9ee5ff",
|
||||
@@ -849,7 +849,7 @@
|
||||
"hint": "#7399a3ff",
|
||||
"hint.background": "#123950ff",
|
||||
"hint.border": "#24556fff",
|
||||
"ignored": "#7b7d7fff",
|
||||
"ignored": "#9a9a98ff",
|
||||
"ignored.background": "#464a52ff",
|
||||
"ignored.border": "#53565dff",
|
||||
"info": "#72cffeff",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#8c957dff",
|
||||
"hint.background": "#1e2321ff",
|
||||
"hint.border": "#303a36ff",
|
||||
"ignored": "#998b78ff",
|
||||
"ignored": "#c5b597ff",
|
||||
"ignored.background": "#4c4642ff",
|
||||
"ignored.border": "#5b534dff",
|
||||
"info": "#83a598ff",
|
||||
@@ -485,7 +485,7 @@
|
||||
"hint": "#6a695bff",
|
||||
"hint.background": "#1e2321ff",
|
||||
"hint.border": "#303a36ff",
|
||||
"ignored": "#998b78ff",
|
||||
"ignored": "#c5b597ff",
|
||||
"ignored.background": "#4c4642ff",
|
||||
"ignored.border": "#5b534dff",
|
||||
"info": "#83a598ff",
|
||||
@@ -859,7 +859,7 @@
|
||||
"hint": "#8c957dff",
|
||||
"hint.background": "#1e2321ff",
|
||||
"hint.border": "#303a36ff",
|
||||
"ignored": "#998b78ff",
|
||||
"ignored": "#c5b597ff",
|
||||
"ignored.background": "#4c4642ff",
|
||||
"ignored.border": "#5b534dff",
|
||||
"info": "#83a598ff",
|
||||
@@ -1233,7 +1233,7 @@
|
||||
"hint": "#677562ff",
|
||||
"hint.background": "#d2dee2ff",
|
||||
"hint.border": "#adc5ccff",
|
||||
"ignored": "#897b6eff",
|
||||
"ignored": "#5f5650ff",
|
||||
"ignored.background": "#d9c8a4ff",
|
||||
"ignored.border": "#c8b899ff",
|
||||
"info": "#0b6678ff",
|
||||
@@ -1607,7 +1607,7 @@
|
||||
"hint": "#677562ff",
|
||||
"hint.background": "#d2dee2ff",
|
||||
"hint.border": "#adc5ccff",
|
||||
"ignored": "#897b6eff",
|
||||
"ignored": "#5f5650ff",
|
||||
"ignored.background": "#d9c8a4ff",
|
||||
"ignored.border": "#c8b899ff",
|
||||
"info": "#0b6678ff",
|
||||
@@ -1981,7 +1981,7 @@
|
||||
"hint": "#677562ff",
|
||||
"hint.background": "#d2dee2ff",
|
||||
"hint.border": "#adc5ccff",
|
||||
"ignored": "#897b6eff",
|
||||
"ignored": "#5f5650ff",
|
||||
"ignored.background": "#d9c8a4ff",
|
||||
"ignored.border": "#c8b899ff",
|
||||
"info": "#0b6678ff",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#5a6f89ff",
|
||||
"hint.background": "#18243dff",
|
||||
"hint.border": "#293b5bff",
|
||||
"ignored": "#555a63ff",
|
||||
"ignored": "#838994ff",
|
||||
"ignored.background": "#3b414dff",
|
||||
"ignored.border": "#464b57ff",
|
||||
"info": "#74ade8ff",
|
||||
@@ -485,7 +485,7 @@
|
||||
"hint": "#9294beff",
|
||||
"hint.background": "#e2e2faff",
|
||||
"hint.border": "#cbcdf6ff",
|
||||
"ignored": "#a1a1a3ff",
|
||||
"ignored": "#7e8087ff",
|
||||
"ignored.background": "#dcdcddff",
|
||||
"ignored.border": "#c9c9caff",
|
||||
"info": "#5c78e2ff",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#5e768cff",
|
||||
"hint.background": "#2f3639ff",
|
||||
"hint.border": "#435255ff",
|
||||
"ignored": "#2f2b43ff",
|
||||
"ignored": "#74708dff",
|
||||
"ignored.background": "#292738ff",
|
||||
"ignored.border": "#423f55ff",
|
||||
"info": "#9bced6ff",
|
||||
@@ -490,7 +490,7 @@
|
||||
"hint": "#7a92aaff",
|
||||
"hint.background": "#dde9ebff",
|
||||
"hint.border": "#c3d7dbff",
|
||||
"ignored": "#938fa3ff",
|
||||
"ignored": "#706c8cff",
|
||||
"ignored.background": "#dcd8d8ff",
|
||||
"ignored.border": "#dcd6d5ff",
|
||||
"info": "#57949fff",
|
||||
@@ -869,7 +869,7 @@
|
||||
"hint": "#728aa2ff",
|
||||
"hint.background": "#2f3639ff",
|
||||
"hint.border": "#435255ff",
|
||||
"ignored": "#605d7aff",
|
||||
"ignored": "#85819eff",
|
||||
"ignored.background": "#38354eff",
|
||||
"ignored.border": "#504c68ff",
|
||||
"info": "#9bced6ff",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#727d68ff",
|
||||
"hint.background": "#171e1eff",
|
||||
"hint.border": "#223131ff",
|
||||
"ignored": "#827568ff",
|
||||
"ignored": "#a69782ff",
|
||||
"ignored.background": "#333944ff",
|
||||
"ignored.border": "#3d4350ff",
|
||||
"info": "#518b8bff",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#4f8297ff",
|
||||
"hint.background": "#141f2cff",
|
||||
"hint.border": "#1b3149ff",
|
||||
"ignored": "#6f8389ff",
|
||||
"ignored": "#93a1a1ff",
|
||||
"ignored.background": "#073743ff",
|
||||
"ignored.border": "#2b4e58ff",
|
||||
"info": "#278ad1ff",
|
||||
@@ -480,7 +480,7 @@
|
||||
"hint": "#5789a3ff",
|
||||
"hint.background": "#dbe6f6ff",
|
||||
"hint.border": "#bfd3efff",
|
||||
"ignored": "#6a7f86ff",
|
||||
"ignored": "#34555eff",
|
||||
"ignored.background": "#cfd0c4ff",
|
||||
"ignored.border": "#9faaa8ff",
|
||||
"info": "#288bd1ff",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hint": "#246e61ff",
|
||||
"hint.background": "#0e2242ff",
|
||||
"hint.border": "#193760ff",
|
||||
"ignored": "#4c4735ff",
|
||||
"ignored": "#736e55ff",
|
||||
"ignored.background": "#2a261cff",
|
||||
"ignored.border": "#302c21ff",
|
||||
"info": "#499befff",
|
||||
|
||||
@@ -16,7 +16,6 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
auto_update.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||
use editor::Editor;
|
||||
use extension::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
|
||||
@@ -99,7 +98,6 @@ impl ActivityIndicator {
|
||||
Box::new(
|
||||
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
|
||||
),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -207,7 +205,7 @@ impl ActivityIndicator {
|
||||
}
|
||||
LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
|
||||
LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
|
||||
LanguageServerBinaryStatus::None => {}
|
||||
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,18 +288,6 @@ impl ActivityIndicator {
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(extension_store) =
|
||||
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
|
||||
{
|
||||
if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!("Updating {extension_id} extension…"),
|
||||
on_click: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
41
crates/ai/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "ai"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/ai.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
bincode = "1.3.3"
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
isahc.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
matrixmultiply = "0.3.7"
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
parse_duration = "2.1.1"
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
8
crates/ai/src/ai.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub mod auth;
|
||||
pub mod completion;
|
||||
pub mod embedding;
|
||||
pub mod models;
|
||||
pub mod prompts;
|
||||
pub mod providers;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
23
crates/ai/src/auth.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use futures::future::BoxFuture;
|
||||
use gpui::AppContext;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ProviderCredential {
|
||||
Credentials { api_key: String },
|
||||
NoCredentials,
|
||||
NotNeeded,
|
||||
}
|
||||
|
||||
pub trait CredentialProvider: Send + Sync {
|
||||
fn has_credentials(&self) -> bool;
|
||||
#[must_use]
|
||||
fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential>;
|
||||
#[must_use]
|
||||
fn save_credentials(
|
||||
&self,
|
||||
cx: &mut AppContext,
|
||||
credential: ProviderCredential,
|
||||
) -> BoxFuture<()>;
|
||||
#[must_use]
|
||||
fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()>;
|
||||
}
|
||||
23
crates/ai/src/completion.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use anyhow::Result;
|
||||
use futures::{future::BoxFuture, stream::BoxStream};
|
||||
|
||||
use crate::{auth::CredentialProvider, models::LanguageModel};
|
||||
|
||||
pub trait CompletionRequest: Send + Sync {
|
||||
fn data(&self) -> serde_json::Result<String>;
|
||||
}
|
||||
|
||||
pub trait CompletionProvider: CredentialProvider {
|
||||
fn base_model(&self) -> Box<dyn LanguageModel>;
|
||||
fn complete(
|
||||
&self,
|
||||
prompt: Box<dyn CompletionRequest>,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
|
||||
fn box_clone(&self) -> Box<dyn CompletionProvider>;
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn CompletionProvider> {
|
||||
fn clone(&self) -> Box<dyn CompletionProvider> {
|
||||
self.box_clone()
|
||||
}
|
||||
}
|
||||
121
crates/ai/src/embedding.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use ordered_float::OrderedFloat;
|
||||
use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
|
||||
use rusqlite::ToSql;
|
||||
|
||||
use crate::auth::CredentialProvider;
|
||||
use crate::models::LanguageModel;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Embedding(pub Vec<f32>);
|
||||
|
||||
// This is needed for semantic index functionality
|
||||
// Unfortunately it has to live wherever the "Embedding" struct is created.
|
||||
// Keeping this in here though, introduces a 'rusqlite' dependency into AI
|
||||
// which is less than ideal
|
||||
impl FromSql for Embedding {
|
||||
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
|
||||
let bytes = value.as_blob()?;
|
||||
let embedding =
|
||||
bincode::deserialize(bytes).map_err(|err| rusqlite::types::FromSqlError::Other(err))?;
|
||||
Ok(Embedding(embedding))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Embedding {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
let bytes = bincode::serialize(&self.0)
|
||||
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
|
||||
Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
|
||||
}
|
||||
}
|
||||
impl From<Vec<f32>> for Embedding {
|
||||
fn from(value: Vec<f32>) -> Self {
|
||||
Embedding(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Embedding {
|
||||
pub fn similarity(&self, other: &Self) -> OrderedFloat<f32> {
|
||||
let len = self.0.len();
|
||||
assert_eq!(len, other.0.len());
|
||||
|
||||
let mut result = 0.0;
|
||||
unsafe {
|
||||
matrixmultiply::sgemm(
|
||||
1,
|
||||
len,
|
||||
1,
|
||||
1.0,
|
||||
self.0.as_ptr(),
|
||||
len as isize,
|
||||
1,
|
||||
other.0.as_ptr(),
|
||||
1,
|
||||
len as isize,
|
||||
0.0,
|
||||
&mut result as *mut f32,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
OrderedFloat(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait EmbeddingProvider: CredentialProvider {
|
||||
fn base_model(&self) -> Box<dyn LanguageModel>;
|
||||
async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
|
||||
fn max_tokens_per_batch(&self) -> usize;
|
||||
fn rate_limit_expiration(&self) -> Option<Instant>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_similarity(mut rng: StdRng) {
|
||||
assert_eq!(
|
||||
Embedding::from(vec![1., 0., 0., 0., 0.])
|
||||
.similarity(&Embedding::from(vec![0., 1., 0., 0., 0.])),
|
||||
0.
|
||||
);
|
||||
assert_eq!(
|
||||
Embedding::from(vec![2., 0., 0., 0., 0.])
|
||||
.similarity(&Embedding::from(vec![3., 1., 0., 0., 0.])),
|
||||
6.
|
||||
);
|
||||
|
||||
for _ in 0..100 {
|
||||
let size = 1536;
|
||||
let mut a = vec![0.; size];
|
||||
let mut b = vec![0.; size];
|
||||
for (a, b) in a.iter_mut().zip(b.iter_mut()) {
|
||||
*a = rng.gen();
|
||||
*b = rng.gen();
|
||||
}
|
||||
let a = Embedding::from(a);
|
||||
let b = Embedding::from(b);
|
||||
|
||||
assert_eq!(
|
||||
round_to_decimals(a.similarity(&b), 1),
|
||||
round_to_decimals(reference_dot(&a.0, &b.0), 1)
|
||||
);
|
||||
}
|
||||
|
||||
fn round_to_decimals(n: OrderedFloat<f32>, decimal_places: i32) -> f32 {
|
||||
let factor = 10.0_f32.powi(decimal_places);
|
||||
(n * factor).round() / factor
|
||||
}
|
||||
|
||||
fn reference_dot(a: &[f32], b: &[f32]) -> OrderedFloat<f32> {
|
||||
OrderedFloat(a.iter().zip(b.iter()).map(|(a, b)| a * b).sum())
|
||||
}
|
||||
}
|
||||
}
|
||||
16
crates/ai/src/models.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub enum TruncationDirection {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
pub trait LanguageModel {
|
||||
fn name(&self) -> String;
|
||||
fn count_tokens(&self, content: &str) -> anyhow::Result<usize>;
|
||||
fn truncate(
|
||||
&self,
|
||||
content: &str,
|
||||
length: usize,
|
||||
direction: TruncationDirection,
|
||||
) -> anyhow::Result<String>;
|
||||
fn capacity(&self) -> anyhow::Result<usize>;
|
||||
}
|
||||
337
crates/ai/src/prompts/base.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use language::BufferSnapshot;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::models::LanguageModel;
|
||||
use crate::prompts::repository_context::PromptCodeSnippet;
|
||||
|
||||
pub(crate) enum PromptFileType {
|
||||
Text,
|
||||
Code,
|
||||
}
|
||||
|
||||
// TODO: Set this up to manage for defaults well
|
||||
pub struct PromptArguments {
|
||||
pub model: Arc<dyn LanguageModel>,
|
||||
pub user_prompt: Option<String>,
|
||||
pub language_name: Option<String>,
|
||||
pub project_name: Option<String>,
|
||||
pub snippets: Vec<PromptCodeSnippet>,
|
||||
pub reserved_tokens: usize,
|
||||
pub buffer: Option<BufferSnapshot>,
|
||||
pub selected_range: Option<Range<usize>>,
|
||||
}
|
||||
|
||||
impl PromptArguments {
|
||||
pub(crate) fn get_file_type(&self) -> PromptFileType {
|
||||
if self
|
||||
.language_name
|
||||
.as_ref()
|
||||
.map(|name| !["Markdown", "Plain Text"].contains(&name.as_str()))
|
||||
.unwrap_or(true)
|
||||
{
|
||||
PromptFileType::Code
|
||||
} else {
|
||||
PromptFileType::Text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PromptTemplate {
|
||||
fn generate(
|
||||
&self,
|
||||
args: &PromptArguments,
|
||||
max_token_length: Option<usize>,
|
||||
) -> anyhow::Result<(String, usize)>;
|
||||
}
|
||||
|
||||
#[repr(i8)]
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum PromptPriority {
|
||||
/// Ignores truncation.
|
||||
Mandatory,
|
||||
/// Truncates based on priority.
|
||||
Ordered { order: usize },
|
||||
}
|
||||
|
||||
impl PartialOrd for PromptPriority {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for PromptPriority {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match (self, other) {
|
||||
(Self::Mandatory, Self::Mandatory) => std::cmp::Ordering::Equal,
|
||||
(Self::Mandatory, Self::Ordered { .. }) => std::cmp::Ordering::Greater,
|
||||
(Self::Ordered { .. }, Self::Mandatory) => std::cmp::Ordering::Less,
|
||||
(Self::Ordered { order: a }, Self::Ordered { order: b }) => b.cmp(a),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptChain {
|
||||
args: PromptArguments,
|
||||
templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)>,
|
||||
}
|
||||
|
||||
impl PromptChain {
|
||||
pub fn new(
|
||||
args: PromptArguments,
|
||||
templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)>,
|
||||
) -> Self {
|
||||
PromptChain { args, templates }
|
||||
}
|
||||
|
||||
pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> {
|
||||
// Argsort based on Prompt Priority
|
||||
let separator = "\n";
|
||||
let separator_tokens = self.args.model.count_tokens(separator)?;
|
||||
let mut sorted_indices = (0..self.templates.len()).collect::<Vec<_>>();
|
||||
sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0));
|
||||
|
||||
let mut tokens_outstanding = if truncate {
|
||||
Some(self.args.model.capacity()? - self.args.reserved_tokens)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut prompts = vec!["".to_string(); sorted_indices.len()];
|
||||
for idx in sorted_indices {
|
||||
let (_, template) = &self.templates[idx];
|
||||
|
||||
if let Some((template_prompt, prompt_token_count)) =
|
||||
template.generate(&self.args, tokens_outstanding).log_err()
|
||||
{
|
||||
if template_prompt != "" {
|
||||
prompts[idx] = template_prompt;
|
||||
|
||||
if let Some(remaining_tokens) = tokens_outstanding {
|
||||
let new_tokens = prompt_token_count + separator_tokens;
|
||||
tokens_outstanding = if remaining_tokens > new_tokens {
|
||||
Some(remaining_tokens - new_tokens)
|
||||
} else {
|
||||
Some(0)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompts.retain(|x| x != "");
|
||||
|
||||
let full_prompt = prompts.join(separator);
|
||||
let total_token_count = self.args.model.count_tokens(&full_prompt)?;
|
||||
anyhow::Ok((prompts.join(separator), total_token_count))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use crate::models::TruncationDirection;
|
||||
use crate::test::FakeLanguageModel;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn test_prompt_chain() {
|
||||
struct TestPromptTemplate {}
|
||||
impl PromptTemplate for TestPromptTemplate {
|
||||
fn generate(
|
||||
&self,
|
||||
args: &PromptArguments,
|
||||
max_token_length: Option<usize>,
|
||||
) -> anyhow::Result<(String, usize)> {
|
||||
let mut content = "This is a test prompt template".to_string();
|
||||
|
||||
let mut token_count = args.model.count_tokens(&content)?;
|
||||
if let Some(max_token_length) = max_token_length {
|
||||
if token_count > max_token_length {
|
||||
content = args.model.truncate(
|
||||
&content,
|
||||
max_token_length,
|
||||
TruncationDirection::End,
|
||||
)?;
|
||||
token_count = max_token_length;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok((content, token_count))
|
||||
}
|
||||
}
|
||||
|
||||
struct TestLowPriorityTemplate {}
|
||||
impl PromptTemplate for TestLowPriorityTemplate {
|
||||
fn generate(
|
||||
&self,
|
||||
args: &PromptArguments,
|
||||
max_token_length: Option<usize>,
|
||||
) -> anyhow::Result<(String, usize)> {
|
||||
let mut content = "This is a low priority test prompt template".to_string();
|
||||
|
||||
let mut token_count = args.model.count_tokens(&content)?;
|
||||
if let Some(max_token_length) = max_token_length {
|
||||
if token_count > max_token_length {
|
||||
content = args.model.truncate(
|
||||
&content,
|
||||
max_token_length,
|
||||
TruncationDirection::End,
|
||||
)?;
|
||||
token_count = max_token_length;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok((content, token_count))
|
||||
}
|
||||
}
|
||||
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel { capacity: 100 });
|
||||
let args = PromptArguments {
|
||||
model: model.clone(),
|
||||
language_name: None,
|
||||
project_name: None,
|
||||
snippets: Vec::new(),
|
||||
reserved_tokens: 0,
|
||||
buffer: None,
|
||||
selected_range: None,
|
||||
user_prompt: None,
|
||||
};
|
||||
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(TestPromptTemplate {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(TestLowPriorityTemplate {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
|
||||
let (prompt, token_count) = chain.generate(false).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
prompt,
|
||||
"This is a test prompt template\nThis is a low priority test prompt template"
|
||||
.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(model.count_tokens(&prompt).unwrap(), token_count);
|
||||
|
||||
// Testing with Truncation Off
|
||||
// Should ignore capacity and return all prompts
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel { capacity: 20 });
|
||||
let args = PromptArguments {
|
||||
model: model.clone(),
|
||||
language_name: None,
|
||||
project_name: None,
|
||||
snippets: Vec::new(),
|
||||
reserved_tokens: 0,
|
||||
buffer: None,
|
||||
selected_range: None,
|
||||
user_prompt: None,
|
||||
};
|
||||
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(TestPromptTemplate {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(TestLowPriorityTemplate {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
|
||||
let (prompt, token_count) = chain.generate(false).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
prompt,
|
||||
"This is a test prompt template\nThis is a low priority test prompt template"
|
||||
.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(model.count_tokens(&prompt).unwrap(), token_count);
|
||||
|
||||
// Testing with Truncation Off
|
||||
// Should ignore capacity and return all prompts
|
||||
let capacity = 20;
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel { capacity });
|
||||
let args = PromptArguments {
|
||||
model: model.clone(),
|
||||
language_name: None,
|
||||
project_name: None,
|
||||
snippets: Vec::new(),
|
||||
reserved_tokens: 0,
|
||||
buffer: None,
|
||||
selected_range: None,
|
||||
user_prompt: None,
|
||||
};
|
||||
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(TestPromptTemplate {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(TestLowPriorityTemplate {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 2 },
|
||||
Box::new(TestLowPriorityTemplate {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
|
||||
let (prompt, token_count) = chain.generate(true).unwrap();
|
||||
|
||||
assert_eq!(prompt, "This is a test promp".to_string());
|
||||
assert_eq!(token_count, capacity);
|
||||
|
||||
// Change Ordering of Prompts Based on Priority
|
||||
let capacity = 120;
|
||||
let reserved_tokens = 10;
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel { capacity });
|
||||
let args = PromptArguments {
|
||||
model: model.clone(),
|
||||
language_name: None,
|
||||
project_name: None,
|
||||
snippets: Vec::new(),
|
||||
reserved_tokens,
|
||||
buffer: None,
|
||||
selected_range: None,
|
||||
user_prompt: None,
|
||||
};
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(
|
||||
PromptPriority::Mandatory,
|
||||
Box::new(TestLowPriorityTemplate {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(TestPromptTemplate {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(TestLowPriorityTemplate {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
|
||||
let (prompt, token_count) = chain.generate(true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
prompt,
|
||||
"This is a low priority test prompt template\nThis is a test prompt template\nThis is a low priority test prompt "
|
||||
.to_string()
|
||||
);
|
||||
assert_eq!(token_count, capacity - reserved_tokens);
|
||||
}
|
||||
}
|
||||
164
crates/ai/src/prompts/file_context.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use anyhow::anyhow;
|
||||
use language::BufferSnapshot;
|
||||
use language::ToOffset;
|
||||
|
||||
use crate::models::LanguageModel;
|
||||
use crate::models::TruncationDirection;
|
||||
use crate::prompts::base::PromptArguments;
|
||||
use crate::prompts::base::PromptTemplate;
|
||||
use std::fmt::Write;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn retrieve_context(
|
||||
buffer: &BufferSnapshot,
|
||||
selected_range: &Option<Range<usize>>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
max_token_count: Option<usize>,
|
||||
) -> anyhow::Result<(String, usize, bool)> {
|
||||
let mut prompt = String::new();
|
||||
let mut truncated = false;
|
||||
if let Some(selected_range) = selected_range {
|
||||
let start = selected_range.start.to_offset(buffer);
|
||||
let end = selected_range.end.to_offset(buffer);
|
||||
|
||||
let start_window = buffer.text_for_range(0..start).collect::<String>();
|
||||
|
||||
let mut selected_window = String::new();
|
||||
if start == end {
|
||||
write!(selected_window, "<|START|>").unwrap();
|
||||
} else {
|
||||
write!(selected_window, "<|START|").unwrap();
|
||||
}
|
||||
|
||||
write!(
|
||||
selected_window,
|
||||
"{}",
|
||||
buffer.text_for_range(start..end).collect::<String>()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if start != end {
|
||||
write!(selected_window, "|END|>").unwrap();
|
||||
}
|
||||
|
||||
let end_window = buffer.text_for_range(end..buffer.len()).collect::<String>();
|
||||
|
||||
if let Some(max_token_count) = max_token_count {
|
||||
let selected_tokens = model.count_tokens(&selected_window)?;
|
||||
if selected_tokens > max_token_count {
|
||||
return Err(anyhow!(
|
||||
"selected range is greater than model context window, truncation not possible"
|
||||
));
|
||||
};
|
||||
|
||||
let mut remaining_tokens = max_token_count - selected_tokens;
|
||||
let start_window_tokens = model.count_tokens(&start_window)?;
|
||||
let end_window_tokens = model.count_tokens(&end_window)?;
|
||||
let outside_tokens = start_window_tokens + end_window_tokens;
|
||||
if outside_tokens > remaining_tokens {
|
||||
let (start_goal_tokens, end_goal_tokens) =
|
||||
if start_window_tokens < end_window_tokens {
|
||||
let start_goal_tokens = (remaining_tokens / 2).min(start_window_tokens);
|
||||
remaining_tokens -= start_goal_tokens;
|
||||
let end_goal_tokens = remaining_tokens.min(end_window_tokens);
|
||||
(start_goal_tokens, end_goal_tokens)
|
||||
} else {
|
||||
let end_goal_tokens = (remaining_tokens / 2).min(end_window_tokens);
|
||||
remaining_tokens -= end_goal_tokens;
|
||||
let start_goal_tokens = remaining_tokens.min(start_window_tokens);
|
||||
(start_goal_tokens, end_goal_tokens)
|
||||
};
|
||||
|
||||
let truncated_start_window =
|
||||
model.truncate(&start_window, start_goal_tokens, TruncationDirection::Start)?;
|
||||
let truncated_end_window =
|
||||
model.truncate(&end_window, end_goal_tokens, TruncationDirection::End)?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"{truncated_start_window}{selected_window}{truncated_end_window}"
|
||||
)
|
||||
.unwrap();
|
||||
truncated = true;
|
||||
} else {
|
||||
writeln!(prompt, "{start_window}{selected_window}{end_window}").unwrap();
|
||||
}
|
||||
} else {
|
||||
// If we dont have a selected range, include entire file.
|
||||
writeln!(prompt, "{}", &buffer.text()).unwrap();
|
||||
|
||||
// Dumb truncation strategy
|
||||
if let Some(max_token_count) = max_token_count {
|
||||
if model.count_tokens(&prompt)? > max_token_count {
|
||||
truncated = true;
|
||||
prompt = model.truncate(&prompt, max_token_count, TruncationDirection::End)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let token_count = model.count_tokens(&prompt)?;
|
||||
anyhow::Ok((prompt, token_count, truncated))
|
||||
}
|
||||
|
||||
pub struct FileContext {}
|
||||
|
||||
impl PromptTemplate for FileContext {
|
||||
fn generate(
|
||||
&self,
|
||||
args: &PromptArguments,
|
||||
max_token_length: Option<usize>,
|
||||
) -> anyhow::Result<(String, usize)> {
|
||||
if let Some(buffer) = &args.buffer {
|
||||
let mut prompt = String::new();
|
||||
// Add Initial Preamble
|
||||
// TODO: Do we want to add the path in here?
|
||||
writeln!(
|
||||
prompt,
|
||||
"The file you are currently working on has the following content:"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let language_name = args
|
||||
.language_name
|
||||
.clone()
|
||||
.unwrap_or("".to_string())
|
||||
.to_lowercase();
|
||||
|
||||
let (context, _, truncated) = retrieve_context(
|
||||
buffer,
|
||||
&args.selected_range,
|
||||
args.model.clone(),
|
||||
max_token_length,
|
||||
)?;
|
||||
writeln!(prompt, "```{language_name}\n{context}\n```").unwrap();
|
||||
|
||||
if truncated {
|
||||
writeln!(prompt, "Note the content has been truncated and only represents a portion of the file.").unwrap();
|
||||
}
|
||||
|
||||
if let Some(selected_range) = &args.selected_range {
|
||||
let start = selected_range.start.to_offset(buffer);
|
||||
let end = selected_range.end.to_offset(buffer);
|
||||
|
||||
if start == end {
|
||||
writeln!(prompt, "In particular, the user's cursor is currently on the '<|START|>' span in the above content, with no text selected.").unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Really dumb truncation strategy
|
||||
if let Some(max_tokens) = max_token_length {
|
||||
prompt = args
|
||||
.model
|
||||
.truncate(&prompt, max_tokens, TruncationDirection::End)?;
|
||||
}
|
||||
|
||||
let token_count = args.model.count_tokens(&prompt)?;
|
||||
anyhow::Ok((prompt, token_count))
|
||||
} else {
|
||||
Err(anyhow!("no buffer provided to retrieve file context from"))
|
||||
}
|
||||
}
|
||||
}
|
||||
99
crates/ai/src/prompts/generate.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::prompts::base::{PromptArguments, PromptFileType, PromptTemplate};
|
||||
use anyhow::anyhow;
|
||||
use std::fmt::Write;
|
||||
|
||||
pub fn capitalize(s: &str) -> String {
|
||||
let mut c = s.chars();
|
||||
match c.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GenerateInlineContent {}
|
||||
|
||||
impl PromptTemplate for GenerateInlineContent {
|
||||
fn generate(
|
||||
&self,
|
||||
args: &PromptArguments,
|
||||
max_token_length: Option<usize>,
|
||||
) -> anyhow::Result<(String, usize)> {
|
||||
let Some(user_prompt) = &args.user_prompt else {
|
||||
return Err(anyhow!("user prompt not provided"));
|
||||
};
|
||||
|
||||
let file_type = args.get_file_type();
|
||||
let content_type = match &file_type {
|
||||
PromptFileType::Code => "code",
|
||||
PromptFileType::Text => "text",
|
||||
};
|
||||
|
||||
let mut prompt = String::new();
|
||||
|
||||
if let Some(selected_range) = &args.selected_range {
|
||||
if selected_range.start == selected_range.end {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
capitalize(content_type)
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(prompt, "Double check that you only return code and not the '<|START|' and '|END|'> spans").unwrap();
|
||||
}
|
||||
} else {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if let Some(language_name) = &args.language_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
match file_type {
|
||||
PromptFileType::Code => {
|
||||
// writeln!(prompt, "Always wrap your code in a Markdown block.").unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Really dumb truncation strategy
|
||||
if let Some(max_tokens) = max_token_length {
|
||||
prompt = args.model.truncate(
|
||||
&prompt,
|
||||
max_tokens,
|
||||
crate::models::TruncationDirection::End,
|
||||
)?;
|
||||
}
|
||||
|
||||
let token_count = args.model.count_tokens(&prompt)?;
|
||||
|
||||
anyhow::Ok((prompt, token_count))
|
||||
}
|
||||
}
|
||||
5
crates/ai/src/prompts/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod base;
|
||||
pub mod file_context;
|
||||
pub mod generate;
|
||||
pub mod preamble;
|
||||
pub mod repository_context;
|
||||
52
crates/ai/src/prompts/preamble.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::prompts::base::{PromptArguments, PromptFileType, PromptTemplate};
|
||||
use std::fmt::Write;
|
||||
|
||||
pub struct EngineerPreamble {}
|
||||
|
||||
impl PromptTemplate for EngineerPreamble {
|
||||
fn generate(
|
||||
&self,
|
||||
args: &PromptArguments,
|
||||
max_token_length: Option<usize>,
|
||||
) -> anyhow::Result<(String, usize)> {
|
||||
let mut prompts = Vec::new();
|
||||
|
||||
match args.get_file_type() {
|
||||
PromptFileType::Code => {
|
||||
prompts.push(format!(
|
||||
"You are an expert {}engineer.",
|
||||
args.language_name.clone().unwrap_or("".to_string()) + " "
|
||||
));
|
||||
}
|
||||
PromptFileType::Text => {
|
||||
prompts.push("You are an expert engineer.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(project_name) = args.project_name.clone() {
|
||||
prompts.push(format!(
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(mut remaining_tokens) = max_token_length {
|
||||
let mut prompt = String::new();
|
||||
let mut total_count = 0;
|
||||
for prompt_piece in prompts {
|
||||
let prompt_token_count =
|
||||
args.model.count_tokens(&prompt_piece)? + args.model.count_tokens("\n")?;
|
||||
if remaining_tokens > prompt_token_count {
|
||||
writeln!(prompt, "{prompt_piece}").unwrap();
|
||||
remaining_tokens -= prompt_token_count;
|
||||
total_count += prompt_token_count;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok((prompt, total_count))
|
||||
} else {
|
||||
let prompt = prompts.join("\n");
|
||||
let token_count = args.model.count_tokens(&prompt)?;
|
||||
anyhow::Ok((prompt, token_count))
|
||||
}
|
||||
}
|
||||
}
|
||||
96
crates/ai/src/prompts/repository_context.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use crate::prompts::base::{PromptArguments, PromptTemplate};
|
||||
use std::fmt::Write;
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
use gpui::{AsyncAppContext, Model};
|
||||
use language::{Anchor, Buffer};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PromptCodeSnippet {
|
||||
path: Option<PathBuf>,
|
||||
language_name: Option<String>,
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl PromptCodeSnippet {
|
||||
pub fn new(
|
||||
buffer: Model<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
let (content, language_name, file_path) = buffer.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let content = snapshot.text_for_range(range.clone()).collect::<String>();
|
||||
|
||||
let language_name = buffer
|
||||
.language()
|
||||
.map(|language| language.name().to_string().to_lowercase());
|
||||
|
||||
let file_path = buffer.file().map(|file| file.path().to_path_buf());
|
||||
|
||||
(content, language_name, file_path)
|
||||
})?;
|
||||
|
||||
anyhow::Ok(PromptCodeSnippet {
|
||||
path: file_path,
|
||||
language_name,
|
||||
content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for PromptCodeSnippet {
|
||||
fn to_string(&self) -> String {
|
||||
let path = self
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.unwrap_or("".to_string());
|
||||
let language_name = self.language_name.clone().unwrap_or("".to_string());
|
||||
let content = self.content.clone();
|
||||
|
||||
format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RepositoryContext {}
|
||||
|
||||
impl PromptTemplate for RepositoryContext {
|
||||
fn generate(
|
||||
&self,
|
||||
args: &PromptArguments,
|
||||
max_token_length: Option<usize>,
|
||||
) -> anyhow::Result<(String, usize)> {
|
||||
const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500;
|
||||
let template = "You are working inside a large repository, here are a few code snippets that may be useful.";
|
||||
let mut prompt = String::new();
|
||||
|
||||
let mut remaining_tokens = max_token_length;
|
||||
let separator_token_length = args.model.count_tokens("\n")?;
|
||||
for snippet in &args.snippets {
|
||||
let mut snippet_prompt = template.to_string();
|
||||
let content = snippet.to_string();
|
||||
writeln!(snippet_prompt, "{content}").unwrap();
|
||||
|
||||
let token_count = args.model.count_tokens(&snippet_prompt)?;
|
||||
if token_count <= MAXIMUM_SNIPPET_TOKEN_COUNT {
|
||||
if let Some(tokens_left) = remaining_tokens {
|
||||
if tokens_left >= token_count {
|
||||
writeln!(prompt, "{snippet_prompt}").unwrap();
|
||||
remaining_tokens = if tokens_left >= (token_count + separator_token_length)
|
||||
{
|
||||
Some(tokens_left - token_count - separator_token_length)
|
||||
} else {
|
||||
Some(0)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
writeln!(prompt, "{snippet_prompt}").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total_token_count = args.model.count_tokens(&prompt)?;
|
||||
anyhow::Ok((prompt, total_token_count))
|
||||
}
|
||||
}
|
||||
1
crates/ai/src/providers.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod open_ai;
|
||||
9
crates/ai/src/providers/open_ai.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod completion;
|
||||
pub mod embedding;
|
||||
pub mod model;
|
||||
|
||||
pub use completion::*;
|
||||
pub use embedding::*;
|
||||
pub use model::OpenAiLanguageModel;
|
||||
|
||||
pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
|
||||
421
crates/ai/src/providers/open_ai/completion.rs
Normal file
@@ -0,0 +1,421 @@
|
||||
use std::{
|
||||
env,
|
||||
fmt::{self, Display},
|
||||
io,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{
|
||||
future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
|
||||
Stream, StreamExt,
|
||||
};
|
||||
use gpui::{AppContext, BackgroundExecutor};
|
||||
use isahc::{http::StatusCode, Request, RequestExt};
|
||||
use parking_lot::RwLock;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::providers::open_ai::{OpenAiLanguageModel, OPEN_AI_API_URL};
|
||||
use crate::{
|
||||
auth::{CredentialProvider, ProviderCredential},
|
||||
completion::{CompletionProvider, CompletionRequest},
|
||||
models::LanguageModel,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn cycle(&mut self) {
|
||||
*self = match self {
|
||||
Role::User => Role::Assistant,
|
||||
Role::Assistant => Role::System,
|
||||
Role::System => Role::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Role::User => write!(f, "User"),
|
||||
Role::Assistant => write!(f, "Assistant"),
|
||||
Role::System => write!(f, "System"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct RequestMessage {
|
||||
pub role: Role,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct OpenAiRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
pub stop: Vec<String>,
|
||||
pub temperature: f32,
|
||||
}
|
||||
|
||||
impl CompletionRequest for OpenAiRequest {
|
||||
fn data(&self) -> serde_json::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct ResponseMessage {
|
||||
pub role: Option<Role>,
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct OpenAiUsage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatChoiceDelta {
|
||||
pub index: u32,
|
||||
pub delta: ResponseMessage,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct OpenAiResponseStreamEvent {
|
||||
pub id: Option<String>,
|
||||
pub object: String,
|
||||
pub created: u32,
|
||||
pub model: String,
|
||||
pub choices: Vec<ChatChoiceDelta>,
|
||||
pub usage: Option<OpenAiUsage>,
|
||||
}
|
||||
|
||||
async fn stream_completion(
|
||||
api_url: String,
|
||||
kind: OpenAiCompletionProviderKind,
|
||||
credential: ProviderCredential,
|
||||
executor: BackgroundExecutor,
|
||||
request: Box<dyn CompletionRequest>,
|
||||
) -> Result<impl Stream<Item = Result<OpenAiResponseStreamEvent>>> {
|
||||
let api_key = match credential {
|
||||
ProviderCredential::Credentials { api_key } => api_key,
|
||||
_ => {
|
||||
return Err(anyhow!("no credentials provider for completion"));
|
||||
}
|
||||
};
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAiResponseStreamEvent>>();
|
||||
|
||||
let (auth_header_name, auth_header_value) = kind.auth_header(api_key);
|
||||
let json_data = request.data()?;
|
||||
let mut response = Request::post(kind.completions_endpoint_url(&api_url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header(auth_header_name, auth_header_value)
|
||||
.body(json_data)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if status == StatusCode::OK {
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||
|
||||
fn parse_line(
|
||||
line: Result<String, io::Error>,
|
||||
) -> Result<Option<OpenAiResponseStreamEvent>> {
|
||||
if let Some(data) = line?.strip_prefix("data: ") {
|
||||
let event = serde_json::from_str(data)?;
|
||||
Ok(Some(event))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = lines.next().await {
|
||||
if let Some(event) = parse_line(line).transpose() {
|
||||
let done = event.as_ref().map_or(false, |event| {
|
||||
event
|
||||
.choices
|
||||
.last()
|
||||
.map_or(false, |choice| choice.finish_reason.is_some())
|
||||
});
|
||||
if tx.unbounded_send(event).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(rx)
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiResponse {
|
||||
error: OpenAiError,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
match serde_json::from_str::<OpenAiResponse>(&body) {
|
||||
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {}",
|
||||
response.error.message,
|
||||
)),
|
||||
|
||||
_ => Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {} {}",
|
||||
response.status(),
|
||||
body,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum AzureOpenAiApiVersion {
|
||||
/// Retiring April 2, 2024.
|
||||
#[serde(rename = "2023-03-15-preview")]
|
||||
V2023_03_15Preview,
|
||||
#[serde(rename = "2023-05-15")]
|
||||
V2023_05_15,
|
||||
/// Retiring April 2, 2024.
|
||||
#[serde(rename = "2023-06-01-preview")]
|
||||
V2023_06_01Preview,
|
||||
/// Retiring April 2, 2024.
|
||||
#[serde(rename = "2023-07-01-preview")]
|
||||
V2023_07_01Preview,
|
||||
/// Retiring April 2, 2024.
|
||||
#[serde(rename = "2023-08-01-preview")]
|
||||
V2023_08_01Preview,
|
||||
/// Retiring April 2, 2024.
|
||||
#[serde(rename = "2023-09-01-preview")]
|
||||
V2023_09_01Preview,
|
||||
#[serde(rename = "2023-12-01-preview")]
|
||||
V2023_12_01Preview,
|
||||
#[serde(rename = "2024-02-15-preview")]
|
||||
V2024_02_15Preview,
|
||||
}
|
||||
|
||||
impl fmt::Display for AzureOpenAiApiVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::V2023_03_15Preview => "2023-03-15-preview",
|
||||
Self::V2023_05_15 => "2023-05-15",
|
||||
Self::V2023_06_01Preview => "2023-06-01-preview",
|
||||
Self::V2023_07_01Preview => "2023-07-01-preview",
|
||||
Self::V2023_08_01Preview => "2023-08-01-preview",
|
||||
Self::V2023_09_01Preview => "2023-09-01-preview",
|
||||
Self::V2023_12_01Preview => "2023-12-01-preview",
|
||||
Self::V2024_02_15Preview => "2024-02-15-preview",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum OpenAiCompletionProviderKind {
|
||||
OpenAi,
|
||||
AzureOpenAi {
|
||||
deployment_id: String,
|
||||
api_version: AzureOpenAiApiVersion,
|
||||
},
|
||||
}
|
||||
|
||||
impl OpenAiCompletionProviderKind {
|
||||
/// Returns the chat completion endpoint URL for this [`OpenAiCompletionProviderKind`].
|
||||
fn completions_endpoint_url(&self, api_url: &str) -> String {
|
||||
match self {
|
||||
Self::OpenAi => {
|
||||
// https://platform.openai.com/docs/api-reference/chat/create
|
||||
format!("{api_url}/chat/completions")
|
||||
}
|
||||
Self::AzureOpenAi {
|
||||
deployment_id,
|
||||
api_version,
|
||||
} => {
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions
|
||||
format!("{api_url}/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the authentication header for this [`OpenAiCompletionProviderKind`].
|
||||
fn auth_header(&self, api_key: String) -> (&'static str, String) {
|
||||
match self {
|
||||
Self::OpenAi => ("Authorization", format!("Bearer {api_key}")),
|
||||
Self::AzureOpenAi { .. } => ("Api-Key", api_key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenAiCompletionProvider {
|
||||
api_url: String,
|
||||
kind: OpenAiCompletionProviderKind,
|
||||
model: OpenAiLanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl OpenAiCompletionProvider {
|
||||
pub async fn new(
|
||||
api_url: String,
|
||||
kind: OpenAiCompletionProviderKind,
|
||||
model_name: String,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Self {
|
||||
let model = executor
|
||||
.spawn(async move { OpenAiLanguageModel::load(&model_name) })
|
||||
.await;
|
||||
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
|
||||
Self {
|
||||
api_url,
|
||||
kind,
|
||||
model,
|
||||
credential,
|
||||
executor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialProvider for OpenAiCompletionProvider {
|
||||
fn has_credentials(&self) -> bool {
|
||||
match *self.credential.read() {
|
||||
ProviderCredential::Credentials { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
|
||||
let existing_credential = self.credential.read().clone();
|
||||
let retrieved_credential = match existing_credential {
|
||||
ProviderCredential::Credentials { .. } => {
|
||||
return async move { existing_credential }.boxed()
|
||||
}
|
||||
_ => {
|
||||
if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() {
|
||||
async move { ProviderCredential::Credentials { api_key } }.boxed()
|
||||
} else {
|
||||
let credentials = cx.read_credentials(OPEN_AI_API_URL);
|
||||
async move {
|
||||
if let Some(Some((_, api_key))) = credentials.await.log_err() {
|
||||
if let Some(api_key) = String::from_utf8(api_key).log_err() {
|
||||
ProviderCredential::Credentials { api_key }
|
||||
} else {
|
||||
ProviderCredential::NoCredentials
|
||||
}
|
||||
} else {
|
||||
ProviderCredential::NoCredentials
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async move {
|
||||
let retrieved_credential = retrieved_credential.await;
|
||||
*self.credential.write() = retrieved_credential.clone();
|
||||
retrieved_credential
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn save_credentials(
|
||||
&self,
|
||||
cx: &mut AppContext,
|
||||
credential: ProviderCredential,
|
||||
) -> BoxFuture<()> {
|
||||
*self.credential.write() = credential.clone();
|
||||
let credential = credential.clone();
|
||||
let write_credentials = match credential {
|
||||
ProviderCredential::Credentials { api_key } => {
|
||||
Some(cx.write_credentials(OPEN_AI_API_URL, "Bearer", api_key.as_bytes()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
async move {
|
||||
if let Some(write_credentials) = write_credentials {
|
||||
write_credentials.await.log_err();
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()> {
|
||||
*self.credential.write() = ProviderCredential::NoCredentials;
|
||||
let delete_credentials = cx.delete_credentials(OPEN_AI_API_URL);
|
||||
async move {
|
||||
delete_credentials.await.log_err();
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for OpenAiCompletionProvider {
|
||||
fn base_model(&self) -> Box<dyn LanguageModel> {
|
||||
let model: Box<dyn LanguageModel> = Box::new(self.model.clone());
|
||||
model
|
||||
}
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
prompt: Box<dyn CompletionRequest>,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
// Currently the CompletionRequest for OpenAI, includes a 'model' parameter
|
||||
// This means that the model is determined by the CompletionRequest and not the CompletionProvider,
|
||||
// which is currently model based, due to the language model.
|
||||
// At some point in the future we should rectify this.
|
||||
let credential = self.credential.read().clone();
|
||||
let api_url = self.api_url.clone();
|
||||
let kind = self.kind.clone();
|
||||
let request = stream_completion(api_url, kind, credential, self.executor.clone(), prompt);
|
||||
async move {
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn box_clone(&self) -> Box<dyn CompletionProvider> {
|
||||
Box::new((*self).clone())
|
||||
}
|
||||
}
|
||||
345
crates/ai/src/providers/open_ai/embedding.rs
Normal file
@@ -0,0 +1,345 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::AsyncReadExt;
|
||||
use futures::FutureExt;
|
||||
use gpui::AppContext;
|
||||
use gpui::BackgroundExecutor;
|
||||
use isahc::http::StatusCode;
|
||||
use isahc::prelude::Configurable;
|
||||
use isahc::{AsyncBody, Response};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use parse_duration::parse;
|
||||
use postage::watch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::env;
|
||||
use std::ops::Add;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use tiktoken_rs::{cl100k_base, CoreBPE};
|
||||
use util::http::{HttpClient, Request};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::auth::{CredentialProvider, ProviderCredential};
|
||||
use crate::embedding::{Embedding, EmbeddingProvider};
|
||||
use crate::models::LanguageModel;
|
||||
use crate::providers::open_ai::OpenAiLanguageModel;
|
||||
|
||||
use crate::providers::open_ai::OPEN_AI_API_URL;
|
||||
|
||||
pub(crate) fn open_ai_bpe_tokenizer() -> &'static CoreBPE {
|
||||
static OPEN_AI_BPE_TOKENIZER: OnceLock<CoreBPE> = OnceLock::new();
|
||||
OPEN_AI_BPE_TOKENIZER.get_or_init(|| cl100k_base().unwrap())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenAiEmbeddingProvider {
|
||||
api_url: String,
|
||||
model: OpenAiLanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
pub client: Arc<dyn HttpClient>,
|
||||
pub executor: BackgroundExecutor,
|
||||
rate_limit_count_rx: watch::Receiver<Option<Instant>>,
|
||||
rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OpenAiEmbeddingRequest<'a> {
|
||||
model: &'static str,
|
||||
input: Vec<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiEmbeddingResponse {
|
||||
data: Vec<OpenAiEmbedding>,
|
||||
usage: OpenAiEmbeddingUsage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiEmbedding {
|
||||
embedding: Vec<f32>,
|
||||
index: usize,
|
||||
object: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiEmbeddingUsage {
|
||||
prompt_tokens: usize,
|
||||
total_tokens: usize,
|
||||
}
|
||||
|
||||
impl OpenAiEmbeddingProvider {
|
||||
pub async fn new(
|
||||
api_url: String,
|
||||
client: Arc<dyn HttpClient>,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Self {
|
||||
let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
|
||||
let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
|
||||
|
||||
// Loading the model is expensive, so ensure this runs off the main thread.
|
||||
let model = executor
|
||||
.spawn(async move { OpenAiLanguageModel::load("text-embedding-ada-002") })
|
||||
.await;
|
||||
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
|
||||
|
||||
OpenAiEmbeddingProvider {
|
||||
api_url,
|
||||
model,
|
||||
credential,
|
||||
client,
|
||||
executor,
|
||||
rate_limit_count_rx,
|
||||
rate_limit_count_tx,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_api_key(&self) -> Result<String> {
|
||||
match self.credential.read().clone() {
|
||||
ProviderCredential::Credentials { api_key } => Ok(api_key),
|
||||
_ => Err(anyhow!("api credentials not provided")),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_rate_limit(&self) {
|
||||
let reset_time = *self.rate_limit_count_tx.lock().borrow();
|
||||
|
||||
if let Some(reset_time) = reset_time {
|
||||
if Instant::now() >= reset_time {
|
||||
*self.rate_limit_count_tx.lock().borrow_mut() = None
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"resolving reset time: {:?}",
|
||||
*self.rate_limit_count_tx.lock().borrow()
|
||||
);
|
||||
}
|
||||
|
||||
fn update_reset_time(&self, reset_time: Instant) {
|
||||
let original_time = *self.rate_limit_count_tx.lock().borrow();
|
||||
|
||||
let updated_time = if let Some(original_time) = original_time {
|
||||
if reset_time < original_time {
|
||||
Some(reset_time)
|
||||
} else {
|
||||
Some(original_time)
|
||||
}
|
||||
} else {
|
||||
Some(reset_time)
|
||||
};
|
||||
|
||||
log::trace!("updating rate limit time: {:?}", updated_time);
|
||||
|
||||
*self.rate_limit_count_tx.lock().borrow_mut() = updated_time;
|
||||
}
|
||||
async fn send_request(
|
||||
&self,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
spans: Vec<&str>,
|
||||
request_timeout: u64,
|
||||
) -> Result<Response<AsyncBody>> {
|
||||
let request = Request::post(format!("{api_url}/embeddings"))
|
||||
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
||||
.timeout(Duration::from_secs(request_timeout))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(
|
||||
serde_json::to_string(&OpenAiEmbeddingRequest {
|
||||
input: spans.clone(),
|
||||
model: "text-embedding-ada-002",
|
||||
})
|
||||
.unwrap()
|
||||
.into(),
|
||||
)?;
|
||||
|
||||
Ok(self.client.send(request).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialProvider for OpenAiEmbeddingProvider {
|
||||
fn has_credentials(&self) -> bool {
|
||||
match *self.credential.read() {
|
||||
ProviderCredential::Credentials { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
|
||||
let existing_credential = self.credential.read().clone();
|
||||
let retrieved_credential = match existing_credential {
|
||||
ProviderCredential::Credentials { .. } => {
|
||||
return async move { existing_credential }.boxed()
|
||||
}
|
||||
_ => {
|
||||
if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() {
|
||||
async move { ProviderCredential::Credentials { api_key } }.boxed()
|
||||
} else {
|
||||
let credentials = cx.read_credentials(OPEN_AI_API_URL);
|
||||
async move {
|
||||
if let Some(Some((_, api_key))) = credentials.await.log_err() {
|
||||
if let Some(api_key) = String::from_utf8(api_key).log_err() {
|
||||
ProviderCredential::Credentials { api_key }
|
||||
} else {
|
||||
ProviderCredential::NoCredentials
|
||||
}
|
||||
} else {
|
||||
ProviderCredential::NoCredentials
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async move {
|
||||
let retrieved_credential = retrieved_credential.await;
|
||||
*self.credential.write() = retrieved_credential.clone();
|
||||
retrieved_credential
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn save_credentials(
|
||||
&self,
|
||||
cx: &mut AppContext,
|
||||
credential: ProviderCredential,
|
||||
) -> BoxFuture<()> {
|
||||
*self.credential.write() = credential.clone();
|
||||
let credential = credential.clone();
|
||||
let write_credentials = match credential {
|
||||
ProviderCredential::Credentials { api_key } => {
|
||||
Some(cx.write_credentials(OPEN_AI_API_URL, "Bearer", api_key.as_bytes()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
async move {
|
||||
if let Some(write_credentials) = write_credentials {
|
||||
write_credentials.await.log_err();
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()> {
|
||||
*self.credential.write() = ProviderCredential::NoCredentials;
|
||||
let delete_credentials = cx.delete_credentials(OPEN_AI_API_URL);
|
||||
async move {
|
||||
delete_credentials.await.log_err();
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EmbeddingProvider for OpenAiEmbeddingProvider {
|
||||
fn base_model(&self) -> Box<dyn LanguageModel> {
|
||||
let model: Box<dyn LanguageModel> = Box::new(self.model.clone());
|
||||
model
|
||||
}
|
||||
|
||||
fn max_tokens_per_batch(&self) -> usize {
|
||||
50000
|
||||
}
|
||||
|
||||
fn rate_limit_expiration(&self) -> Option<Instant> {
|
||||
*self.rate_limit_count_rx.borrow()
|
||||
}
|
||||
|
||||
async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
|
||||
const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
|
||||
const MAX_RETRIES: usize = 4;
|
||||
|
||||
let api_url = self.api_url.as_str();
|
||||
let api_key = self.get_api_key()?;
|
||||
|
||||
let mut request_number = 0;
|
||||
let mut rate_limiting = false;
|
||||
let mut request_timeout: u64 = 15;
|
||||
let mut response: Response<AsyncBody>;
|
||||
while request_number < MAX_RETRIES {
|
||||
response = self
|
||||
.send_request(
|
||||
&api_url,
|
||||
&api_key,
|
||||
spans.iter().map(|x| &**x).collect(),
|
||||
request_timeout,
|
||||
)
|
||||
.await?;
|
||||
|
||||
request_number += 1;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::REQUEST_TIMEOUT => {
|
||||
request_timeout += 5;
|
||||
}
|
||||
StatusCode::OK => {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
let response: OpenAiEmbeddingResponse = serde_json::from_str(&body)?;
|
||||
|
||||
log::trace!(
|
||||
"openai embedding completed. tokens: {:?}",
|
||||
response.usage.total_tokens
|
||||
);
|
||||
|
||||
// If we complete a request successfully that was previously rate_limited
|
||||
// resolve the rate limit
|
||||
if rate_limiting {
|
||||
self.resolve_rate_limit()
|
||||
}
|
||||
|
||||
return Ok(response
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|embedding| Embedding::from(embedding.embedding))
|
||||
.collect());
|
||||
}
|
||||
StatusCode::TOO_MANY_REQUESTS => {
|
||||
rate_limiting = true;
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
let delay_duration = {
|
||||
let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
|
||||
if let Some(time_to_reset) =
|
||||
response.headers().get("x-ratelimit-reset-tokens")
|
||||
{
|
||||
if let Ok(time_str) = time_to_reset.to_str() {
|
||||
parse(time_str).unwrap_or(delay)
|
||||
} else {
|
||||
delay
|
||||
}
|
||||
} else {
|
||||
delay
|
||||
}
|
||||
};
|
||||
|
||||
// If we've previously rate limited, increment the duration but not the count
|
||||
let reset_time = Instant::now().add(delay_duration);
|
||||
self.update_reset_time(reset_time);
|
||||
|
||||
log::trace!(
|
||||
"openai rate limiting: waiting {:?} until lifted",
|
||||
&delay_duration
|
||||
);
|
||||
|
||||
self.executor.timer(delay_duration).await;
|
||||
}
|
||||
_ => {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Err(anyhow!(
|
||||
"open ai bad request: {:?} {:?}",
|
||||
&response.status(),
|
||||
body
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(anyhow!("openai max retries"))
|
||||
}
|
||||
}
|
||||
59
crates/ai/src/providers/open_ai/model.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use anyhow::anyhow;
|
||||
use tiktoken_rs::CoreBPE;
|
||||
|
||||
use crate::models::{LanguageModel, TruncationDirection};
|
||||
|
||||
use super::open_ai_bpe_tokenizer;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenAiLanguageModel {
|
||||
name: String,
|
||||
bpe: Option<CoreBPE>,
|
||||
}
|
||||
|
||||
impl OpenAiLanguageModel {
|
||||
pub fn load(model_name: &str) -> Self {
|
||||
let bpe = tiktoken_rs::get_bpe_from_model(model_name)
|
||||
.unwrap_or(open_ai_bpe_tokenizer().to_owned());
|
||||
OpenAiLanguageModel {
|
||||
name: model_name.to_string(),
|
||||
bpe: Some(bpe),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModel for OpenAiLanguageModel {
|
||||
fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
fn count_tokens(&self, content: &str) -> anyhow::Result<usize> {
|
||||
if let Some(bpe) = &self.bpe {
|
||||
anyhow::Ok(bpe.encode_with_special_tokens(content).len())
|
||||
} else {
|
||||
Err(anyhow!("bpe for open ai model was not retrieved"))
|
||||
}
|
||||
}
|
||||
fn truncate(
|
||||
&self,
|
||||
content: &str,
|
||||
length: usize,
|
||||
direction: TruncationDirection,
|
||||
) -> anyhow::Result<String> {
|
||||
if let Some(bpe) = &self.bpe {
|
||||
let tokens = bpe.encode_with_special_tokens(content);
|
||||
if tokens.len() > length {
|
||||
match direction {
|
||||
TruncationDirection::End => bpe.decode(tokens[..length].to_vec()),
|
||||
TruncationDirection::Start => bpe.decode(tokens[length..].to_vec()),
|
||||
}
|
||||
} else {
|
||||
bpe.decode(tokens)
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("bpe for open ai model was not retrieved"))
|
||||
}
|
||||
}
|
||||
fn capacity(&self) -> anyhow::Result<usize> {
|
||||
anyhow::Ok(tiktoken_rs::model::get_context_size(&self.name))
|
||||
}
|
||||
}
|
||||
206
crates/ai/src/test.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use std::{
|
||||
sync::atomic::{self, AtomicUsize, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::AppContext;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
auth::{CredentialProvider, ProviderCredential},
|
||||
completion::{CompletionProvider, CompletionRequest},
|
||||
embedding::{Embedding, EmbeddingProvider},
|
||||
models::{LanguageModel, TruncationDirection},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FakeLanguageModel {
|
||||
pub capacity: usize,
|
||||
}
|
||||
|
||||
impl LanguageModel for FakeLanguageModel {
|
||||
fn name(&self) -> String {
|
||||
"dummy".to_string()
|
||||
}
|
||||
fn count_tokens(&self, content: &str) -> anyhow::Result<usize> {
|
||||
anyhow::Ok(content.chars().collect::<Vec<char>>().len())
|
||||
}
|
||||
fn truncate(
|
||||
&self,
|
||||
content: &str,
|
||||
length: usize,
|
||||
direction: TruncationDirection,
|
||||
) -> anyhow::Result<String> {
|
||||
println!("TRYING TO TRUNCATE: {:?}", length.clone());
|
||||
|
||||
if length > self.count_tokens(content)? {
|
||||
println!("NOT TRUNCATING");
|
||||
return anyhow::Ok(content.to_string());
|
||||
}
|
||||
|
||||
anyhow::Ok(match direction {
|
||||
TruncationDirection::End => content.chars().collect::<Vec<char>>()[..length]
|
||||
.into_iter()
|
||||
.collect::<String>(),
|
||||
TruncationDirection::Start => content.chars().collect::<Vec<char>>()[length..]
|
||||
.into_iter()
|
||||
.collect::<String>(),
|
||||
})
|
||||
}
|
||||
fn capacity(&self) -> anyhow::Result<usize> {
|
||||
anyhow::Ok(self.capacity)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FakeEmbeddingProvider {
|
||||
pub embedding_count: AtomicUsize,
|
||||
}
|
||||
|
||||
impl Clone for FakeEmbeddingProvider {
|
||||
fn clone(&self) -> Self {
|
||||
FakeEmbeddingProvider {
|
||||
embedding_count: AtomicUsize::new(self.embedding_count.load(Ordering::SeqCst)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeEmbeddingProvider {
|
||||
pub fn embedding_count(&self) -> usize {
|
||||
self.embedding_count.load(atomic::Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn embed_sync(&self, span: &str) -> Embedding {
|
||||
let mut result = vec![1.0; 26];
|
||||
for letter in span.chars() {
|
||||
let letter = letter.to_ascii_lowercase();
|
||||
if letter as u32 >= 'a' as u32 {
|
||||
let ix = (letter as u32) - ('a' as u32);
|
||||
if ix < 26 {
|
||||
result[ix as usize] += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
for x in &mut result {
|
||||
*x /= norm;
|
||||
}
|
||||
|
||||
result.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialProvider for FakeEmbeddingProvider {
|
||||
fn has_credentials(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn retrieve_credentials(&self, _cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
|
||||
async { ProviderCredential::NotNeeded }.boxed()
|
||||
}
|
||||
|
||||
fn save_credentials(
|
||||
&self,
|
||||
_cx: &mut AppContext,
|
||||
_credential: ProviderCredential,
|
||||
) -> BoxFuture<()> {
|
||||
async {}.boxed()
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, _cx: &mut AppContext) -> BoxFuture<()> {
|
||||
async {}.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EmbeddingProvider for FakeEmbeddingProvider {
|
||||
fn base_model(&self) -> Box<dyn LanguageModel> {
|
||||
Box::new(FakeLanguageModel { capacity: 1000 })
|
||||
}
|
||||
fn max_tokens_per_batch(&self) -> usize {
|
||||
1000
|
||||
}
|
||||
|
||||
fn rate_limit_expiration(&self) -> Option<Instant> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn embed_batch(&self, spans: Vec<String>) -> anyhow::Result<Vec<Embedding>> {
|
||||
self.embedding_count
|
||||
.fetch_add(spans.len(), atomic::Ordering::SeqCst);
|
||||
|
||||
anyhow::Ok(spans.iter().map(|span| self.embed_sync(span)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeCompletionProvider {
|
||||
last_completion_tx: Mutex<Option<mpsc::Sender<String>>>,
|
||||
}
|
||||
|
||||
impl Clone for FakeCompletionProvider {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
last_completion_tx: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeCompletionProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_completion_tx: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_completion(&self, completion: impl Into<String>) {
|
||||
let mut tx = self.last_completion_tx.lock();
|
||||
tx.as_mut().unwrap().try_send(completion.into()).unwrap();
|
||||
}
|
||||
|
||||
pub fn finish_completion(&self) {
|
||||
self.last_completion_tx.lock().take().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialProvider for FakeCompletionProvider {
|
||||
fn has_credentials(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn retrieve_credentials(&self, _cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
|
||||
async { ProviderCredential::NotNeeded }.boxed()
|
||||
}
|
||||
|
||||
fn save_credentials(
|
||||
&self,
|
||||
_cx: &mut AppContext,
|
||||
_credential: ProviderCredential,
|
||||
) -> BoxFuture<()> {
|
||||
async {}.boxed()
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, _cx: &mut AppContext) -> BoxFuture<()> {
|
||||
async {}.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for FakeCompletionProvider {
|
||||
fn base_model(&self) -> Box<dyn LanguageModel> {
|
||||
let model: Box<dyn LanguageModel> = Box::new(FakeLanguageModel { capacity: 8190 });
|
||||
model
|
||||
}
|
||||
fn complete(
|
||||
&self,
|
||||
_prompt: Box<dyn CompletionRequest>,
|
||||
) -> BoxFuture<'static, anyhow::Result<BoxStream<'static, anyhow::Result<String>>>> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
*self.last_completion_tx.lock() = Some(tx);
|
||||
async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed()
|
||||
}
|
||||
fn box_clone(&self) -> Box<dyn CompletionProvider> {
|
||||
Box::new((*self).clone())
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "anthropic"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/anthropic.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-AGPL
|
||||
@@ -1,234 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub enum Model {
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-opus-20240229")]
|
||||
Claude3Opus,
|
||||
#[serde(rename = "claude-3-sonnet-20240229")]
|
||||
Claude3Sonnet,
|
||||
#[serde(rename = "claude-3-haiku-20240307")]
|
||||
Claude3Haiku,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
if id.starts_with("claude-3-opus") {
|
||||
Ok(Self::Claude3Opus)
|
||||
} else if id.starts_with("claude-3-sonnet") {
|
||||
Ok(Self::Claude3Sonnet)
|
||||
} else if id.starts_with("claude-3-haiku") {
|
||||
Ok(Self::Claude3Haiku)
|
||||
} else {
|
||||
Err(anyhow!("Invalid model id: {}", id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
200_000
|
||||
}
|
||||
}
|
||||
|
||||
#[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}'")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
pub model: Model,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
pub system: String,
|
||||
pub max_tokens: u32,
|
||||
}
|
||||
|
||||
#[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,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let request = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("Anthropic-Beta", "messages-2023-12-15")
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(AsyncBody::from(serde_json::to_string(&request)?))?;
|
||||
let mut response = client.send(request).await?;
|
||||
if response.status().is_success() {
|
||||
let reader = BufReader::new(response.into_body());
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(|line| async move {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
let line = line.strip_prefix("data: ")?;
|
||||
match serde_json::from_str(line) {
|
||||
Ok(response) => Some(Ok(response)),
|
||||
Err(error) => Some(Err(anyhow!(error))),
|
||||
}
|
||||
}
|
||||
Err(error) => Some(Err(anyhow!(error))),
|
||||
}
|
||||
})
|
||||
.boxed())
|
||||
} else {
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
|
||||
let body_str = std::str::from_utf8(&body)?;
|
||||
|
||||
match serde_json::from_str::<ResponseEvent>(body_str) {
|
||||
Ok(_) => Err(anyhow!(
|
||||
"Unexpected success response while expecting an error: {}",
|
||||
body_str,
|
||||
)),
|
||||
Err(_) => Err(anyhow!(
|
||||
"Failed to connect to API: {} {}",
|
||||
response.status(),
|
||||
body_str,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use util::http::IsahcHttpClient;
|
||||
|
||||
// #[tokio::test]
|
||||
// async fn stream_completion_success() {
|
||||
// let http_client = IsahcHttpClient::new().unwrap();
|
||||
|
||||
// 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,
|
||||
// };
|
||||
|
||||
// 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();
|
||||
|
||||
// stream
|
||||
// .for_each(|event| async {
|
||||
// match event {
|
||||
// Ok(event) => println!("{:?}", event),
|
||||
// Err(e) => eprintln!("Error: {:?}", e),
|
||||
// }
|
||||
// })
|
||||
// .await;
|
||||
// }
|
||||
// }
|
||||
@@ -5,9 +5,6 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
path = "src/assets.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
|
||||
use anyhow::anyhow;
|
||||
|
||||
use gpui::{AppContext, AssetSource, Result, SharedString};
|
||||
use gpui::{AssetSource, Result, SharedString};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
@@ -34,19 +34,3 @@ impl AssetSource for Assets {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Assets {
|
||||
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
|
||||
pub fn load_fonts(&self, cx: &AppContext) -> gpui::Result<()> {
|
||||
let font_paths = self.list("fonts")?;
|
||||
let mut embedded_fonts = Vec::new();
|
||||
for font_path in font_paths {
|
||||
if font_path.ends_with(".ttf") {
|
||||
let font_bytes = cx.asset_source().load(&font_path)?;
|
||||
embedded_fonts.push(font_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
cx.text_system().add_fonts(embedded_fonts)
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,19 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ai.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
file_icons.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
@@ -25,13 +26,12 @@ language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -45,6 +45,7 @@ uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ai = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod completion_provider;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod streaming_diff;
|
||||
|
||||
mod embedded_scope;
|
||||
|
||||
use ai::providers::open_ai::Role;
|
||||
use anyhow::Result;
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
use assistant_settings::{AssistantSettings, OpenAiModel, ZedDotDevModel};
|
||||
use assistant_settings::OpenAiModel;
|
||||
use chrono::{DateTime, Local};
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
use gpui::{actions, AppContext, BorrowAppContext, Global, SharedString};
|
||||
pub(crate) use saved_conversation::*;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{actions, AppContext, SharedString};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
@@ -35,6 +30,7 @@ actions!(
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -43,138 +39,6 @@ actions!(
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn cycle(&mut self) {
|
||||
*self = match self {
|
||||
Role::User => Role::Assistant,
|
||||
Role::Assistant => Role::System,
|
||||
Role::System => Role::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Role::User => write!(f, "user"),
|
||||
Role::Assistant => write!(f, "assistant"),
|
||||
Role::System => write!(f, "system"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum LanguageModel {
|
||||
ZedDotDev(ZedDotDevModel),
|
||||
OpenAi(OpenAiModel),
|
||||
}
|
||||
|
||||
impl Default for LanguageModel {
|
||||
fn default() -> Self {
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModel {
|
||||
pub fn telemetry_id(&self) -> String {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
|
||||
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.id()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> String {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => format!("openai/{}", model.display_name()),
|
||||
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.display_name()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.max_token_count(),
|
||||
LanguageModel::ZedDotDev(model) => model.max_token_count(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.id(),
|
||||
LanguageModel::ZedDotDev(model) => model.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct LanguageModelRequestMessage {
|
||||
pub role: Role,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl LanguageModelRequestMessage {
|
||||
pub fn to_proto(&self) -> proto::LanguageModelRequestMessage {
|
||||
proto::LanguageModelRequestMessage {
|
||||
role: match self.role {
|
||||
Role::User => proto::LanguageModelRole::LanguageModelUser,
|
||||
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
|
||||
Role::System => proto::LanguageModelRole::LanguageModelSystem,
|
||||
} as i32,
|
||||
content: self.content.clone(),
|
||||
tool_calls: Vec::new(),
|
||||
tool_call_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct LanguageModelRequest {
|
||||
pub model: LanguageModel,
|
||||
pub messages: Vec<LanguageModelRequestMessage>,
|
||||
pub stop: Vec<String>,
|
||||
pub temperature: f32,
|
||||
}
|
||||
|
||||
impl LanguageModelRequest {
|
||||
pub fn to_proto(&self) -> proto::CompleteWithLanguageModel {
|
||||
proto::CompleteWithLanguageModel {
|
||||
model: self.model.id().to_string(),
|
||||
messages: self.messages.iter().map(|m| m.to_proto()).collect(),
|
||||
stop: self.stop.clone(),
|
||||
temperature: self.temperature,
|
||||
tool_choice: None,
|
||||
tools: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct LanguageModelResponseMessage {
|
||||
pub role: Option<Role>,
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct LanguageModelUsage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct LanguageModelChoiceDelta {
|
||||
pub index: u32,
|
||||
pub delta: LanguageModelResponseMessage,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
@@ -189,61 +53,72 @@ enum MessageStatus {
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// The state pertaining to the Assistant.
|
||||
#[derive(Default)]
|
||||
struct Assistant {
|
||||
/// Whether the Assistant is enabled.
|
||||
enabled: bool,
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedMessage {
|
||||
id: MessageId,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
impl Global for Assistant {}
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversation {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
impl Assistant {
|
||||
const NAMESPACE: &'static str = "assistant";
|
||||
impl SavedConversation {
|
||||
const VERSION: &'static str = "0.1.0";
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
|
||||
if self.enabled == enabled {
|
||||
return;
|
||||
struct SavedConversationMetadata {
|
||||
title: String,
|
||||
path: PathBuf,
|
||||
mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
self.enabled = enabled;
|
||||
|
||||
if !enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Self::NAMESPACE);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_namespace(Self::NAMESPACE);
|
||||
});
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.set_global(Assistant::default());
|
||||
AssistantSettings::register(cx);
|
||||
completion_provider::init(client, cx);
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
});
|
||||
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
});
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,305 +1,169 @@
|
||||
use std::fmt;
|
||||
|
||||
use ai::providers::open_ai::{
|
||||
AzureOpenAiApiVersion, OpenAiCompletionProviderKind, OPEN_AI_API_URL,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use gpui::Pixels;
|
||||
pub use open_ai::Model as OpenAiModel;
|
||||
use schemars::{
|
||||
schema::{InstanceType, Metadata, Schema, SchemaObject},
|
||||
JsonSchema,
|
||||
};
|
||||
use serde::{
|
||||
de::{self, Visitor},
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum ZedDotDevModel {
|
||||
Gpt3Point5Turbo,
|
||||
Gpt4,
|
||||
#[default]
|
||||
Gpt4Turbo,
|
||||
Claude3Opus,
|
||||
Claude3Sonnet,
|
||||
Claude3Haiku,
|
||||
Custom(String),
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OpenAiModel {
|
||||
#[serde(rename = "gpt-3.5-turbo-0613")]
|
||||
ThreePointFiveTurbo,
|
||||
#[serde(rename = "gpt-4-0613")]
|
||||
Four,
|
||||
#[serde(rename = "gpt-4-1106-preview")]
|
||||
FourTurbo,
|
||||
}
|
||||
|
||||
impl Serialize for ZedDotDevModel {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ZedDotDevModelVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
|
||||
type Value = ZedDotDevModel;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match value {
|
||||
"gpt-3.5-turbo" => Ok(ZedDotDevModel::Gpt3Point5Turbo),
|
||||
"gpt-4" => Ok(ZedDotDevModel::Gpt4),
|
||||
"gpt-4-turbo-preview" => Ok(ZedDotDevModel::Gpt4Turbo),
|
||||
_ => Ok(ZedDotDevModel::Custom(value.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(ZedDotDevModelVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for ZedDotDevModel {
|
||||
fn schema_name() -> String {
|
||||
"ZedDotDevModel".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
let variants = vec![
|
||||
"gpt-3.5-turbo".to_owned(),
|
||||
"gpt-4".to_owned(),
|
||||
"gpt-4-turbo-preview".to_owned(),
|
||||
];
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
enum_values: Some(variants.into_iter().map(|s| s.into()).collect()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
title: Some("ZedDotDevModel".to_owned()),
|
||||
default: Some(serde_json::json!("gpt-4-turbo-preview")),
|
||||
examples: vec![
|
||||
serde_json::json!("gpt-3.5-turbo"),
|
||||
serde_json::json!("gpt-4"),
|
||||
serde_json::json!("gpt-4-turbo-preview"),
|
||||
serde_json::json!("custom-model-name"),
|
||||
],
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ZedDotDevModel {
|
||||
pub fn id(&self) -> &str {
|
||||
impl OpenAiModel {
|
||||
pub fn full_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
|
||||
Self::Gpt4 => "gpt-4",
|
||||
Self::Gpt4Turbo => "gpt-4-turbo-preview",
|
||||
Self::Claude3Opus => "claude-3-opus",
|
||||
Self::Claude3Sonnet => "claude-3-sonnet",
|
||||
Self::Claude3Haiku => "claude-3-haiku",
|
||||
Self::Custom(id) => id,
|
||||
Self::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
|
||||
Self::Four => "gpt-4-0613",
|
||||
Self::FourTurbo => "gpt-4-1106-preview",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Gpt3Point5Turbo => "GPT 3.5 Turbo",
|
||||
Self::Gpt4 => "GPT 4",
|
||||
Self::Gpt4Turbo => "GPT 4 Turbo",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
Self::Custom(id) => id.as_str(),
|
||||
Self::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
Self::Four => "gpt-4",
|
||||
Self::FourTurbo => "gpt-4-turbo",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
pub fn cycle(&self) -> Self {
|
||||
match self {
|
||||
Self::Gpt3Point5Turbo => 2048,
|
||||
Self::Gpt4 => 4096,
|
||||
Self::Gpt4Turbo => 128000,
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 200000,
|
||||
Self::Custom(_) => 4096, // TODO: Make this configurable
|
||||
Self::ThreePointFiveTurbo => Self::Four,
|
||||
Self::Four => Self::FourTurbo,
|
||||
Self::FourTurbo => Self::ThreePointFiveTurbo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
#[default]
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProvider {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev {
|
||||
#[serde(default)]
|
||||
default_model: ZedDotDevModel,
|
||||
},
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
#[serde(default)]
|
||||
default_model: OpenAiModel,
|
||||
#[serde(default = "open_ai_url")]
|
||||
api_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for AssistantProvider {
|
||||
fn default() -> Self {
|
||||
Self::ZedDotDev {
|
||||
default_model: ZedDotDevModel::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_ai_url() -> String {
|
||||
"https://api.openai.com/v1".into()
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
pub button: bool,
|
||||
/// Where to dock the assistant.
|
||||
pub dock: AssistantDockPosition,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
pub default_width: Pixels,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
pub default_height: Pixels,
|
||||
pub provider: AssistantProvider,
|
||||
/// The default OpenAI model to use when starting new conversations.
|
||||
#[deprecated = "Please use `provider.default_model` instead."]
|
||||
pub default_open_ai_model: OpenAiModel,
|
||||
/// OpenAI API base URL to use when starting new conversations.
|
||||
#[deprecated = "Please use `provider.api_url` instead."]
|
||||
pub openai_api_url: String,
|
||||
/// The settings for the AI provider.
|
||||
pub provider: AiProviderSettings,
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContent {
|
||||
Versioned(VersionedAssistantSettingsContent),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
impl AssistantSettings {
|
||||
pub fn provider_kind(&self) -> anyhow::Result<OpenAiCompletionProviderKind> {
|
||||
match &self.provider {
|
||||
AiProviderSettings::OpenAi(_) => Ok(OpenAiCompletionProviderKind::OpenAi),
|
||||
AiProviderSettings::AzureOpenAi(settings) => {
|
||||
let deployment_id = settings
|
||||
.deployment_id
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("no Azure OpenAI deployment ID"))?;
|
||||
let api_version = settings
|
||||
.api_version
|
||||
.ok_or_else(|| anyhow!("no Azure OpenAI API version"))?;
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::Versioned(VersionedAssistantSettingsContent::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
fn upgrade(&self) -> AssistantSettingsContentV1 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV1 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
provider: if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
|
||||
Some(AssistantProvider::OpenAi {
|
||||
default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
|
||||
api_url: open_ai_api_url.clone(),
|
||||
})
|
||||
} else {
|
||||
settings.default_open_ai_model.clone().map(|open_ai_model| {
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: open_ai_model,
|
||||
api_url: open_ai_url(),
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
Ok(OpenAiCompletionProviderKind::AzureOpenAi {
|
||||
deployment_id,
|
||||
api_version,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
}
|
||||
pub fn provider_api_url(&self) -> anyhow::Result<String> {
|
||||
match &self.provider {
|
||||
AiProviderSettings::OpenAi(settings) => Ok(settings
|
||||
.api_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| OPEN_AI_API_URL.to_string())),
|
||||
AiProviderSettings::AzureOpenAi(settings) => settings
|
||||
.api_url
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("no Azure OpenAI API URL")),
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V1(AssistantSettingsContentV1 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
provider: None,
|
||||
})
|
||||
pub fn provider_model(&self) -> anyhow::Result<OpenAiModel> {
|
||||
match &self.provider {
|
||||
AiProviderSettings::OpenAi(settings) => {
|
||||
Ok(settings.default_model.unwrap_or(OpenAiModel::FourTurbo))
|
||||
}
|
||||
AiProviderSettings::AzureOpenAi(settings) => {
|
||||
let deployment_id = settings
|
||||
.deployment_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no Azure OpenAI deployment ID"))?;
|
||||
|
||||
match deployment_id {
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-4-and-gpt-4-turbo-preview
|
||||
"gpt-4" | "gpt-4-32k" => Ok(OpenAiModel::Four),
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-35
|
||||
"gpt-35-turbo" | "gpt-35-turbo-16k" | "gpt-35-turbo-instruct" => {
|
||||
Ok(OpenAiModel::ThreePointFiveTurbo)
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"no matching OpenAI model found for deployment ID: '{deployment_id}'"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provider_model_name(&self) -> anyhow::Result<String> {
|
||||
match &self.provider {
|
||||
AiProviderSettings::OpenAi(settings) => Ok(settings
|
||||
.default_model
|
||||
.unwrap_or(OpenAiModel::FourTurbo)
|
||||
.full_name()
|
||||
.to_string()),
|
||||
AiProviderSettings::AzureOpenAi(settings) => settings
|
||||
.deployment_id
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("no Azure OpenAI deployment ID")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the assistant service.
|
||||
///
|
||||
/// This can either be the internal `zed.dev` service or an external `openai` service,
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProvider>,
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct LegacyAssistantSettingsContent {
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
@@ -316,164 +180,88 @@ pub struct LegacyAssistantSettingsContent {
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
/// Deprecated: Please use `provider.default_model` instead.
|
||||
/// The default OpenAI model to use when starting new conversations.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
#[deprecated = "Please use `provider.default_model` instead."]
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// Deprecated: Please use `provider.api_url` instead.
|
||||
/// OpenAI API base URL to use when starting new conversations.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
#[deprecated = "Please use `provider.api_url` instead."]
|
||||
pub openai_api_url: Option<String>,
|
||||
/// The settings for the AI provider.
|
||||
#[serde(default)]
|
||||
pub provider: AiProviderSettingsContent,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AiProviderSettings {
|
||||
/// The settings for the OpenAI provider.
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi(OpenAiProviderSettings),
|
||||
/// The settings for the Azure OpenAI provider.
|
||||
#[serde(rename = "azure_openai")]
|
||||
AzureOpenAi(AzureOpenAiProviderSettings),
|
||||
}
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
/// The settings for the AI provider used by the Zed Assistant.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AiProviderSettingsContent {
|
||||
/// The settings for the OpenAI provider.
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi(OpenAiProviderSettingsContent),
|
||||
/// The settings for the Azure OpenAI provider.
|
||||
#[serde(rename = "azure_openai")]
|
||||
AzureOpenAi(AzureOpenAiProviderSettingsContent),
|
||||
}
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
&mut settings.default_width,
|
||||
value.default_width.map(Into::into),
|
||||
);
|
||||
merge(
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
if let Some(provider) = value.provider.clone() {
|
||||
match (&mut settings.provider, provider) {
|
||||
(
|
||||
AssistantProvider::ZedDotDev { default_model },
|
||||
AssistantProvider::ZedDotDev {
|
||||
default_model: default_model_override,
|
||||
},
|
||||
) => {
|
||||
*default_model = default_model_override;
|
||||
}
|
||||
(
|
||||
AssistantProvider::OpenAi {
|
||||
default_model,
|
||||
api_url,
|
||||
},
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: default_model_override,
|
||||
api_url: api_url_override,
|
||||
},
|
||||
) => {
|
||||
*default_model = default_model_override;
|
||||
*api_url = api_url_override;
|
||||
}
|
||||
(merged, provider_override) => {
|
||||
*merged = provider_override;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
impl Default for AiProviderSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::OpenAi(OpenAiProviderSettingsContent::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OpenAiProviderSettings {
|
||||
/// The OpenAI API base URL to use when starting new conversations.
|
||||
pub api_url: Option<String>,
|
||||
/// The default OpenAI model to use when starting new conversations.
|
||||
pub default_model: Option<OpenAiModel>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AppContext, BorrowAppContext};
|
||||
use settings::SettingsStore;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_deserialize_assistant_settings(cx: &mut AppContext) {
|
||||
let store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
|
||||
// Settings default to gpt-4-turbo.
|
||||
AssistantSettings::register(cx);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: OpenAiModel::FourTurbo,
|
||||
api_url: open_ai_url()
|
||||
}
|
||||
);
|
||||
|
||||
// Ensure backward-compatibility.
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store
|
||||
.set_user_settings(
|
||||
r#"{
|
||||
"assistant": {
|
||||
"openai_api_url": "test-url",
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: OpenAiModel::FourTurbo,
|
||||
api_url: "test-url".into()
|
||||
}
|
||||
);
|
||||
cx.update_global::<SettingsStore, _>(|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 {
|
||||
default_model: OpenAiModel::Four,
|
||||
api_url: open_ai_url()
|
||||
}
|
||||
);
|
||||
|
||||
// The new version supports setting a custom model when using zed.dev.
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store
|
||||
.set_user_settings(
|
||||
r#"{
|
||||
"assistant": {
|
||||
"version": "1",
|
||||
"provider": {
|
||||
"name": "zed.dev",
|
||||
"default_model": "custom"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::ZedDotDev {
|
||||
default_model: ZedDotDevModel::Custom("custom".into())
|
||||
}
|
||||
);
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct OpenAiProviderSettingsContent {
|
||||
/// The OpenAI API base URL to use when starting new conversations.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
pub api_url: Option<String>,
|
||||
/// The default OpenAI model to use when starting new conversations.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
pub default_model: Option<OpenAiModel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AzureOpenAiProviderSettings {
|
||||
/// The Azure OpenAI API base URL to use when starting new conversations.
|
||||
pub api_url: Option<String>,
|
||||
/// The Azure OpenAI API version.
|
||||
pub api_version: Option<AzureOpenAiApiVersion>,
|
||||
/// The Azure OpenAI API deployment ID.
|
||||
pub deployment_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AzureOpenAiProviderSettingsContent {
|
||||
/// The Azure OpenAI API base URL to use when starting new conversations.
|
||||
pub api_url: Option<String>,
|
||||
/// The Azure OpenAI API version.
|
||||
pub api_version: Option<AzureOpenAiApiVersion>,
|
||||
/// The Azure OpenAI deployment ID.
|
||||
pub deployment_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::{
|
||||
streaming_diff::{Hunk, StreamingDiff},
|
||||
CompletionProvider, LanguageModelRequest,
|
||||
};
|
||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, CompletionRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use std::{cmp, future, ops::Range};
|
||||
use multi_buffer;
|
||||
use std::{cmp, future, ops::Range, sync::Arc};
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
@@ -21,6 +20,7 @@ pub enum CodegenKind {
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
@@ -35,9 +35,15 @@ pub struct Codegen {
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(buffer: Model<MultiBuffer>, kind: CodegenKind, cx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
@@ -88,7 +94,7 @@ impl Codegen {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
|
||||
pub fn start(&mut self, prompt: Box<dyn CompletionRequest>, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
@@ -102,7 +108,7 @@ impl Codegen {
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
|
||||
|
||||
let response = CompletionProvider::global(cx).complete(prompt);
|
||||
let response = self.provider.complete(prompt);
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
@@ -299,7 +305,7 @@ fn strip_invalid_spans_from_codeblock(
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer.is_empty() || buffer == "`" || buffer == "``" {
|
||||
if buffer == "" || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
@@ -354,15 +360,14 @@ fn strip_invalid_spans_from_codeblock(
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::FakeCompletionProvider;
|
||||
|
||||
use super::*;
|
||||
use ai::test::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
language_settings, tree_sitter_rust, Buffer, BufferId, Language, LanguageConfig,
|
||||
LanguageMatcher, Point,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
@@ -373,11 +378,15 @@ mod tests {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl CompletionRequest for DummyCompletionRequest {
|
||||
fn data(&self) -> serde_json::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
@@ -388,17 +397,27 @@ mod tests {
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::new(0, BufferId::new(1).unwrap(), text).with_language(Arc::new(rust_lang()), cx)
|
||||
});
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let codegen =
|
||||
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Transform { range }, cx));
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Transform { range },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
@@ -411,7 +430,8 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
println!("CHUNK: {:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
@@ -436,8 +456,6 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
@@ -446,17 +464,27 @@ mod tests {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::new(0, BufferId::new(1).unwrap(), text).with_language(Arc::new(rust_lang()), cx)
|
||||
});
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let codegen =
|
||||
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Generate { position }, cx));
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
@@ -469,7 +497,7 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
@@ -494,8 +522,6 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
@@ -504,17 +530,27 @@ mod tests {
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::new(0, BufferId::new(1).unwrap(), text).with_language(Arc::new(rust_lang()), cx)
|
||||
});
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let codegen =
|
||||
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Generate { position }, cx));
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
@@ -527,7 +563,8 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
println!("{:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod fake;
|
||||
mod open_ai;
|
||||
mod zed;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use fake::*;
|
||||
pub use open_ai::*;
|
||||
pub use zed::*;
|
||||
|
||||
use crate::{
|
||||
assistant_settings::{AssistantProvider, AssistantSettings},
|
||||
LanguageModel, LanguageModelRequest,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
use futures::{future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let mut settings_version = 0;
|
||||
let provider = match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { default_model } => {
|
||||
CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
client.clone(),
|
||||
settings_version,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
AssistantProvider::OpenAi {
|
||||
default_model,
|
||||
api_url,
|
||||
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
settings_version,
|
||||
)),
|
||||
};
|
||||
cx.set_global(provider);
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
settings_version += 1;
|
||||
cx.update_global::<CompletionProvider, _>(|provider, cx| {
|
||||
match (&mut *provider, &AssistantSettings::get_global(cx).provider) {
|
||||
(
|
||||
CompletionProvider::OpenAi(provider),
|
||||
AssistantProvider::OpenAi {
|
||||
default_model,
|
||||
api_url,
|
||||
},
|
||||
) => {
|
||||
provider.update(default_model.clone(), api_url.clone(), settings_version);
|
||||
}
|
||||
(
|
||||
CompletionProvider::ZedDotDev(provider),
|
||||
AssistantProvider::ZedDotDev { default_model },
|
||||
) => {
|
||||
provider.update(default_model.clone(), settings_version);
|
||||
}
|
||||
(CompletionProvider::OpenAi(_), AssistantProvider::ZedDotDev { default_model }) => {
|
||||
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
client.clone(),
|
||||
settings_version,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
(
|
||||
CompletionProvider::ZedDotDev(_),
|
||||
AssistantProvider::OpenAi {
|
||||
default_model,
|
||||
api_url,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
settings_version,
|
||||
));
|
||||
}
|
||||
#[cfg(test)]
|
||||
(CompletionProvider::Fake(_), _) => unimplemented!(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub enum CompletionProvider {
|
||||
OpenAi(OpenAiCompletionProvider),
|
||||
ZedDotDev(ZedDotDevCompletionProvider),
|
||||
#[cfg(test)]
|
||||
Fake(FakeCompletionProvider),
|
||||
}
|
||||
|
||||
impl gpui::Global for CompletionProvider {}
|
||||
|
||||
impl CompletionProvider {
|
||||
pub fn global(cx: &AppContext) -> &Self {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.settings_version(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.settings_version(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.is_authenticated(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.authenticate(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.authentication_prompt(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::ZedDotDev(_) => Task::ready(Ok(())),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> LanguageModel {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.default_model()),
|
||||
CompletionProvider::ZedDotDev(provider) => {
|
||||
LanguageModel::ZedDotDev(provider.default_model())
|
||||
}
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.complete(request),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.complete(request),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(provider) => provider.complete(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FakeCompletionProvider {
|
||||
current_completion_tx: Arc<parking_lot::Mutex<Option<mpsc::UnboundedSender<String>>>>,
|
||||
}
|
||||
|
||||
impl FakeCompletionProvider {
|
||||
pub fn complete(&self) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
*self.current_completion_tx.lock() = Some(tx);
|
||||
async move { Ok(rx.map(Ok).boxed()) }.boxed()
|
||||
}
|
||||
|
||||
pub fn send_completion(&self, chunk: String) {
|
||||
self.current_completion_tx
|
||||
.lock()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.unbounded_send(chunk)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn finish_completion(&self) {
|
||||
self.current_completion_tx.lock().take();
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
use crate::{
|
||||
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, FontStyle, FontWeight, Task, TextStyle, View, WhiteSpace};
|
||||
use open_ai::{stream_completion, Request, RequestMessage, Role as OpenAiRole};
|
||||
use settings::Settings;
|
||||
use std::{env, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::{http::HttpClient, ResultExt};
|
||||
|
||||
pub struct OpenAiCompletionProvider {
|
||||
api_key: Option<String>,
|
||||
api_url: String,
|
||||
default_model: OpenAiModel,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
settings_version: usize,
|
||||
}
|
||||
|
||||
impl OpenAiCompletionProvider {
|
||||
pub fn new(
|
||||
default_model: OpenAiModel,
|
||||
api_url: String,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
settings_version: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
api_key: None,
|
||||
api_url,
|
||||
default_model,
|
||||
http_client,
|
||||
settings_version,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, default_model: OpenAiModel, api_url: String, settings_version: usize) {
|
||||
self.default_model = default_model;
|
||||
self.api_url = api_url;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.api_key.is_some()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
if self.is_authenticated() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let api_url = self.api_url.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
|
||||
api_key
|
||||
} else {
|
||||
let (_, api_key) = cx
|
||||
.update(|cx| cx.read_credentials(&api_url))?
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("credentials not found"))?;
|
||||
String::from_utf8(api_key)?
|
||||
};
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::OpenAi(provider) = provider {
|
||||
provider.api_key = Some(api_key);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let delete_credentials = cx.delete_credentials(&self.api_url);
|
||||
cx.spawn(|mut cx| async move {
|
||||
delete_credentials.await.log_err();
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::OpenAi(provider) = provider {
|
||||
provider.api_key = None;
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> OpenAiModel {
|
||||
self.default_model.clone()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = self.to_open_ai_request(request);
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
let api_key = self.api_key.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
async move {
|
||||
let api_key = api_key.ok_or_else(|| anyhow!("missing api key"))?;
|
||||
let request = stream_completion(http_client.as_ref(), &api_url, &api_key, request);
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
|
||||
let model = match request.model {
|
||||
LanguageModel::ZedDotDev(_) => self.default_model(),
|
||||
LanguageModel::OpenAi(model) => model,
|
||||
};
|
||||
|
||||
Request {
|
||||
model,
|
||||
messages: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| match msg.role {
|
||||
Role::User => RequestMessage::User {
|
||||
content: msg.content,
|
||||
},
|
||||
Role::Assistant => RequestMessage::Assistant {
|
||||
content: Some(msg.content),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => RequestMessage::System {
|
||||
content: msg.content,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
stream: true,
|
||||
stop: request.stop,
|
||||
temperature: request.temperature,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_open_ai_tokens(
|
||||
request: LanguageModelRequest,
|
||||
background_executor: &gpui::BackgroundExecutor,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
background_executor
|
||||
.spawn(async move {
|
||||
let messages = request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|message| tiktoken_rs::ChatCompletionRequestMessage {
|
||||
role: match message.role {
|
||||
Role::User => "user".into(),
|
||||
Role::Assistant => "assistant".into(),
|
||||
Role::System => "system".into(),
|
||||
},
|
||||
content: Some(message.content),
|
||||
name: None,
|
||||
function_call: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tiktoken_rs::num_tokens_from_messages(request.model.id(), &messages)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
impl From<Role> for open_ai::Role {
|
||||
fn from(val: Role) -> Self {
|
||||
match val {
|
||||
Role::User => OpenAiRole::User,
|
||||
Role::Assistant => OpenAiRole::Assistant,
|
||||
Role::System => OpenAiRole::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
api_key: View<Editor>,
|
||||
api_url: String,
|
||||
}
|
||||
|
||||
impl AuthenticationPrompt {
|
||||
fn new(api_url: String, cx: &mut WindowContext) -> Self {
|
||||
Self {
|
||||
api_key: cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text(
|
||||
"sk-000000000000000000000000000000000000000000000000",
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
}),
|
||||
api_url,
|
||||
}
|
||||
}
|
||||
|
||||
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
let api_key = self.api_key.read(cx).text(cx);
|
||||
if api_key.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let write_credentials = cx.write_credentials(&self.api_url, "Bearer", api_key.as_bytes());
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
write_credentials.await?;
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::OpenAi(provider) = provider {
|
||||
provider.api_key = Some(api_key);
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.api_key,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const INSTRUCTIONS: [&str; 6] = [
|
||||
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
|
||||
" - You can create an API key at: platform.openai.com/api-keys",
|
||||
" - Make sure your OpenAI account has credits",
|
||||
" - Having a subscription for another service like GitHub Copilot won't work.",
|
||||
"",
|
||||
"Paste your OpenAI API key below and hit enter to use the assistant:",
|
||||
];
|
||||
|
||||
v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::save_api_key))
|
||||
.children(
|
||||
INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.my_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"You can also assign the OPENAI_API_KEY environment variable and restart Zed.",
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Click on").size(LabelSize::Small))
|
||||
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
use crate::{
|
||||
assistant_settings::ZedDotDevModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
|
||||
LanguageModelRequest,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, Client};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryFutureExt};
|
||||
use gpui::{AnyView, AppContext, Task};
|
||||
use std::{future, sync::Arc};
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct ZedDotDevCompletionProvider {
|
||||
client: Arc<Client>,
|
||||
default_model: ZedDotDevModel,
|
||||
settings_version: usize,
|
||||
status: client::Status,
|
||||
_maintain_client_status: Task<()>,
|
||||
}
|
||||
|
||||
impl ZedDotDevCompletionProvider {
|
||||
pub fn new(
|
||||
default_model: ZedDotDevModel,
|
||||
client: Arc<Client>,
|
||||
settings_version: usize,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let mut status_rx = client.status();
|
||||
let status = *status_rx.borrow();
|
||||
let maintain_client_status = cx.spawn(|mut cx| async move {
|
||||
while let Some(status) = status_rx.next().await {
|
||||
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::ZedDotDev(provider) = provider {
|
||||
provider.status = status;
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Self {
|
||||
client,
|
||||
default_model,
|
||||
settings_version,
|
||||
status,
|
||||
_maintain_client_status: maintain_client_status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, default_model: ZedDotDevModel, settings_version: usize) {
|
||||
self.default_model = default_model;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> ZedDotDevModel {
|
||||
self.default_model.clone()
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.status.is_connected()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |cx| async move { client.authenticate_and_connect(true, &cx).await })
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|_cx| AuthenticationPrompt).into()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
match request.model {
|
||||
LanguageModel::OpenAi(_) => future::ready(Err(anyhow!("invalid model"))).boxed(),
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Turbo)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt3Point5Turbo) => {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::ZedDotDev(
|
||||
ZedDotDevModel::Claude3Opus
|
||||
| ZedDotDevModel::Claude3Sonnet
|
||||
| ZedDotDevModel::Claude3Haiku,
|
||||
) => {
|
||||
// Can't find a tokenizer for Claude 3, so for now just use the same as OpenAI's as an approximation.
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::Custom(model)) => {
|
||||
let request = self.client.request(proto::CountTokensWithLanguageModel {
|
||||
model,
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.to_proto())
|
||||
.collect(),
|
||||
});
|
||||
async move {
|
||||
let response = request.await?;
|
||||
Ok(response.token_count as usize)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = proto::CompleteWithLanguageModel {
|
||||
model: request.model.id().to_string(),
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.to_proto())
|
||||
.collect(),
|
||||
stop: request.stop,
|
||||
temperature: request.temperature,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
};
|
||||
|
||||
self.client
|
||||
.request_stream(request)
|
||||
.map_ok(|stream| {
|
||||
stream
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta?.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt;
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const LABEL: &str = "Generate and analyze code with language models. You can dialog with the assistant in this panel or transform code inline.";
|
||||
|
||||
v_flex().gap_6().p_4().child(Label::new(LABEL)).child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.on_click(|_, cx| {
|
||||
CompletionProvider::global(cx)
|
||||
.authenticate(cx)
|
||||
.detach_and_log_err(cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
Label::new("Sign in to enable collaboration.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use editor::MultiBuffer;
|
||||
use gpui::{AppContext, Model, ModelContext, Subscription};
|
||||
|
||||
use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EmbeddedScope {
|
||||
active_buffer: Option<Model<MultiBuffer>>,
|
||||
active_buffer_enabled: bool,
|
||||
active_buffer_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl EmbeddedScope {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_buffer: None,
|
||||
active_buffer_enabled: true,
|
||||
active_buffer_subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_buffer(
|
||||
&mut self,
|
||||
buffer: Option<Model<MultiBuffer>>,
|
||||
cx: &mut ModelContext<Conversation>,
|
||||
) {
|
||||
self.active_buffer_subscription.take();
|
||||
|
||||
if let Some(active_buffer) = buffer.clone() {
|
||||
self.active_buffer_subscription =
|
||||
Some(cx.subscribe(&active_buffer, |conversation, _, e, cx| {
|
||||
if let multi_buffer::Event::Edited { .. } = e {
|
||||
conversation.count_remaining_tokens(cx)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
self.active_buffer = buffer;
|
||||
}
|
||||
|
||||
pub fn active_buffer(&self) -> Option<&Model<MultiBuffer>> {
|
||||
self.active_buffer.as_ref()
|
||||
}
|
||||
|
||||
pub fn active_buffer_enabled(&self) -> bool {
|
||||
self.active_buffer_enabled
|
||||
}
|
||||
|
||||
pub fn set_active_buffer_enabled(&mut self, enabled: bool) {
|
||||
self.active_buffer_enabled = enabled;
|
||||
}
|
||||
|
||||
/// Provide a message for the language model based on the active buffer.
|
||||
pub fn message(&self, cx: &AppContext) -> Option<LanguageModelRequestMessage> {
|
||||
if !self.active_buffer_enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let active_buffer = self.active_buffer.as_ref()?;
|
||||
let buffer = active_buffer.read(cx);
|
||||
|
||||
if let Some(singleton) = buffer.as_singleton() {
|
||||
let singleton = singleton.read(cx);
|
||||
|
||||
let filename = singleton
|
||||
.file()
|
||||
.map(|file| file.path().to_string_lossy())
|
||||
.unwrap_or("Untitled".into());
|
||||
|
||||
let text = singleton.text();
|
||||
|
||||
let language = singleton
|
||||
.language()
|
||||
.map(|l| {
|
||||
let name = l.code_fence_block_name();
|
||||
name.to_string()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let markdown =
|
||||
format!("User's active file `{filename}`:\n\n```{language}\n{text}```\n\n");
|
||||
|
||||
return Some(LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: markdown,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,95 +1,394 @@
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range};
|
||||
use ai::models::LanguageModel;
|
||||
use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
|
||||
use ai::prompts::file_context::FileContext;
|
||||
use ai::prompts::generate::GenerateInlineContent;
|
||||
use ai::prompts::preamble::EngineerPreamble;
|
||||
use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext};
|
||||
use ai::providers::open_ai::OpenAiLanguageModel;
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
collapse: Range<usize>,
|
||||
keep: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let selected_range = selected_range.to_offset(buffer);
|
||||
let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
|
||||
Some(&grammar.embedding_config.as_ref()?.query)
|
||||
});
|
||||
let configs = ts_matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|g| g.embedding_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = Vec::new();
|
||||
while let Some(mat) = ts_matches.peek() {
|
||||
let config = &configs[mat.grammar_index];
|
||||
if let Some(collapse) = mat.captures.iter().find_map(|cap| {
|
||||
if Some(cap.index) == config.collapse_capture_ix {
|
||||
Some(cap.node.byte_range())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let mut keep = Vec::new();
|
||||
for capture in mat.captures.iter() {
|
||||
if Some(capture.index) == config.keep_capture_ix {
|
||||
keep.push(capture.node.byte_range());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ts_matches.advance();
|
||||
matches.push(Match { collapse, keep });
|
||||
} else {
|
||||
ts_matches.advance();
|
||||
}
|
||||
}
|
||||
matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
|
||||
let mut matches = matches.into_iter().peekable();
|
||||
|
||||
let mut summary = String::new();
|
||||
let mut offset = 0;
|
||||
let mut flushed_selection = false;
|
||||
while let Some(mat) = matches.next() {
|
||||
// Keep extending the collapsed range if the next match surrounds
|
||||
// the current one.
|
||||
while let Some(next_mat) = matches.peek() {
|
||||
if mat.collapse.start <= next_mat.collapse.start
|
||||
&& mat.collapse.end >= next_mat.collapse.end
|
||||
{
|
||||
matches.next().unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if offset > mat.collapse.start {
|
||||
// Skip collapsed nodes that have already been summarized.
|
||||
offset = cmp::max(offset, mat.collapse.end);
|
||||
continue;
|
||||
}
|
||||
|
||||
if offset <= selected_range.start && selected_range.start <= mat.collapse.end {
|
||||
if !flushed_selection {
|
||||
// The collapsed node ends after the selection starts, so we'll flush the selection first.
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
flushed_selection = true;
|
||||
}
|
||||
|
||||
// If the selection intersects the collapsed node, we won't collapse it.
|
||||
if selected_range.end >= mat.collapse.start {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..mat.collapse.start));
|
||||
for keep in mat.keep {
|
||||
summary.extend(buffer.text_for_range(keep));
|
||||
}
|
||||
offset = mat.collapse.end;
|
||||
}
|
||||
|
||||
// Flush selection if we haven't already done so.
|
||||
if !flushed_selection && offset <= selected_range.start {
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..buffer.len()));
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
search_results: Vec<PromptCodeSnippet>,
|
||||
model: &str,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
// Using new Prompt Templates
|
||||
let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAiLanguageModel::load(model));
|
||||
let lang_name = if let Some(language_name) = language_name {
|
||||
Some(language_name.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
let args = PromptArguments {
|
||||
model: openai_model,
|
||||
language_name: lang_name.clone(),
|
||||
project_name,
|
||||
snippets: search_results.clone(),
|
||||
reserved_tokens: 1000,
|
||||
buffer: Some(buffer),
|
||||
selected_range: Some(range),
|
||||
user_prompt: Some(user_prompt.clone()),
|
||||
};
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(RepositoryContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(FileContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Mandatory,
|
||||
Box::new(GenerateInlineContent {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
let (prompt, _) = chain.generate(true)?;
|
||||
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("|END|>");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.end..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
anyhow::Ok(prompt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use gpui::{AppContext, Context};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, BufferId, Language, LanguageConfig,
|
||||
LanguageMatcher, Point,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_embedding_query(
|
||||
r#"
|
||||
(
|
||||
[(line_comment) (attribute_item)]* @context
|
||||
.
|
||||
[
|
||||
(struct_item
|
||||
name: (_) @name)
|
||||
|
||||
(enum_item
|
||||
name: (_) @name)
|
||||
|
||||
(impl_item
|
||||
trait: (_)? @name
|
||||
"for"? @name
|
||||
type: (_) @name)
|
||||
|
||||
(trait_item
|
||||
name: (_) @name)
|
||||
|
||||
(function_item
|
||||
name: (_) @name
|
||||
body: (block
|
||||
"{" @keep
|
||||
"}" @keep) @collapse)
|
||||
|
||||
(macro_definition
|
||||
name: (_) @name)
|
||||
] @item
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_outline_for_prompt(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language_settings::init(cx);
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
self.a
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::new(0, BufferId::new(1).unwrap(), text).with_language(Arc::new(rust_lang()), cx)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
<|S|>a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let <|S|a |E|>= 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
<|S|>
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
<|S|>"}
|
||||
);
|
||||
|
||||
// Ensure nested functions get collapsed properly.
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
let a = 30;
|
||||
fn nested() -> usize {
|
||||
3
|
||||
}
|
||||
self.a + nested()
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)),
|
||||
indoc! {"
|
||||
<|S|>struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedConversation {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
|
||||
pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
|
||||
let saved_conversation = fs.load(path).await?;
|
||||
let saved_conversation_json =
|
||||
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
||||
match saved_conversation_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
|
||||
"0.1.0" => {
|
||||
let saved_conversation =
|
||||
serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
|
||||
Ok(Self {
|
||||
id: saved_conversation.id,
|
||||
zed: saved_conversation.zed,
|
||||
version: saved_conversation.version,
|
||||
text: saved_conversation.text,
|
||||
messages: saved_conversation.messages,
|
||||
message_metadata: saved_conversation.message_metadata,
|
||||
summary: saved_conversation.summary,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"unrecognized saved conversation version: {}",
|
||||
version
|
||||
)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved conversation")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversationV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
pub struct SavedConversationMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
@@ -197,10 +197,12 @@ impl StreamingDiff {
|
||||
} else {
|
||||
hunks.push(Hunk::Remove { len: char_len })
|
||||
}
|
||||
} else if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
[package]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant2.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
stories = ["dep:story"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tooling.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
project.workspace = true
|
||||
rich_text.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assets.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
languages.workspace = true
|
||||
node_runtime.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
release_channel.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
@@ -1,378 +0,0 @@
|
||||
//! This example creates a basic Chat UI with a function for rolling a die.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use assets::Assets;
|
||||
use assistant2::AssistantPanel;
|
||||
use assistant_tooling::{LanguageModelTool, ToolRegistry};
|
||||
use client::{Client, UserStore};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
|
||||
use language::LanguageRegistry;
|
||||
use project::Project;
|
||||
use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use theme::LoadThemes;
|
||||
use ui::{div, prelude::*, Render};
|
||||
use util::ResultExt as _;
|
||||
|
||||
actions!(example, [Quit]);
|
||||
|
||||
struct RollDiceTool {}
|
||||
|
||||
impl RollDiceTool {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum Die {
|
||||
D6 = 6,
|
||||
D20 = 20,
|
||||
}
|
||||
|
||||
impl Die {
|
||||
fn into_str(&self) -> &'static str {
|
||||
match self {
|
||||
Die::D6 => "d6",
|
||||
Die::D20 => "d20",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
struct DiceParams {
|
||||
/// The number of dice to roll.
|
||||
num_dice: u8,
|
||||
/// Which die to roll. Defaults to a d6 if not provided.
|
||||
die_type: Option<Die>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DieRoll {
|
||||
die: Die,
|
||||
roll: u8,
|
||||
}
|
||||
|
||||
impl DieRoll {
|
||||
fn render(&self) -> AnyElement {
|
||||
match self.die {
|
||||
Die::D6 => {
|
||||
let face = match self.roll {
|
||||
6 => div().child("⚅"),
|
||||
5 => div().child("⚄"),
|
||||
4 => div().child("⚃"),
|
||||
3 => div().child("⚂"),
|
||||
2 => div().child("⚁"),
|
||||
1 => div().child("⚀"),
|
||||
_ => div().child("😅"),
|
||||
};
|
||||
face.text_3xl().into_any_element()
|
||||
}
|
||||
_ => div()
|
||||
.child(format!("{}", self.roll))
|
||||
.text_3xl()
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DiceRoll {
|
||||
rolls: Vec<DieRoll>,
|
||||
}
|
||||
|
||||
pub struct DiceView {
|
||||
result: Result<DiceRoll>,
|
||||
}
|
||||
|
||||
impl Render for DiceView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let output = match &self.result {
|
||||
Ok(output) => output,
|
||||
Err(_) => return "Somehow dice failed 🎲".into_any_element(),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.children(
|
||||
output
|
||||
.rolls
|
||||
.iter()
|
||||
.map(|roll| div().p_2().child(roll.render())),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for RollDiceTool {
|
||||
type Input = DiceParams;
|
||||
type Output = DiceRoll;
|
||||
type View = DiceView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"roll_dice".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Rolls N many dice and returns the results.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<gpui::Result<Self::Output>> {
|
||||
let rolls = (0..input.num_dice)
|
||||
.map(|_| {
|
||||
let die_type = input.die_type.as_ref().unwrap_or(&Die::D6).clone();
|
||||
|
||||
DieRoll {
|
||||
die: die_type.clone(),
|
||||
roll: rand::thread_rng().gen_range(1..=die_type as u8),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Task::ready(Ok(DiceRoll { rolls }));
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| DiceView { result })
|
||||
}
|
||||
|
||||
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
let output = match output {
|
||||
Ok(output) => output,
|
||||
Err(_) => return "Somehow dice failed 🎲".to_string(),
|
||||
};
|
||||
|
||||
let mut result = String::new();
|
||||
for roll in &output.rolls {
|
||||
let die = &roll.die;
|
||||
result.push_str(&format!("{}: {}\n", die.into_str(), roll.roll));
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
struct FileBrowserTool {
|
||||
fs: Arc<dyn Fs>,
|
||||
root_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl FileBrowserTool {
|
||||
fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
|
||||
Self { fs, root_dir }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
struct FileBrowserParams {
|
||||
command: FileBrowserCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
enum FileBrowserCommand {
|
||||
Ls { path: PathBuf },
|
||||
Cat { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum FileBrowserOutput {
|
||||
Ls { entries: Vec<String> },
|
||||
Cat { content: String },
|
||||
}
|
||||
|
||||
pub struct FileBrowserView {
|
||||
result: Result<FileBrowserOutput>,
|
||||
}
|
||||
|
||||
impl Render for FileBrowserView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Ok(output) = self.result.as_ref() else {
|
||||
return h_flex().child("Failed to perform operation");
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => v_flex().children(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|entry| h_flex().text_ui(cx).child(entry.clone())),
|
||||
),
|
||||
FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for FileBrowserTool {
|
||||
type Input = FileBrowserParams;
|
||||
type Output = FileBrowserOutput;
|
||||
type View = FileBrowserView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"file_browser".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"A tool for browsing the filesystem.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<gpui::Result<Self::Output>> {
|
||||
cx.spawn({
|
||||
let fs = self.fs.clone();
|
||||
let root_dir = self.root_dir.clone();
|
||||
let input = input.clone();
|
||||
|_cx| async move {
|
||||
match input.command {
|
||||
FileBrowserCommand::Ls { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let mut output = fs.read_dir(&path).await?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
while let Some(entry) = output.next().await {
|
||||
let entry = entry?;
|
||||
entries.push(entry.display().to_string());
|
||||
}
|
||||
|
||||
Ok(FileBrowserOutput::Ls { entries })
|
||||
}
|
||||
FileBrowserCommand::Cat { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let output = fs.load(&path).await?;
|
||||
|
||||
Ok(FileBrowserOutput::Cat { content: output })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| FileBrowserView { result })
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
let Ok(output) = output else {
|
||||
return "Failed to perform command: {input:?}".to_string();
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => entries.join("\n"),
|
||||
FileBrowserOutput::Cat { content } => content.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
|
||||
cx.on_action(|_: &Quit, cx: &mut AppContext| {
|
||||
cx.quit();
|
||||
});
|
||||
|
||||
settings::init(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init(cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
|
||||
client::init_settings(cx);
|
||||
release_channel::init("0.130.0", cx);
|
||||
|
||||
let client = Client::production(cx);
|
||||
{
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
assistant2::init(client.clone(), cx);
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
cx.update(|cx| {
|
||||
let fs = Arc::new(fs::RealFs::new(None));
|
||||
let cwd = std::env::current_dir().expect("Failed to get current working directory");
|
||||
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(RollDiceTool::new(), cx)
|
||||
.context("failed to register DummyTool")
|
||||
.log_err();
|
||||
|
||||
tool_registry
|
||||
.register(FileBrowserTool::new(fs, cwd), cx)
|
||||
.context("failed to register FileBrowserTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
println!("Tools registered");
|
||||
for definition in tool_registry.definitions() {
|
||||
println!("{}", definition);
|
||||
}
|
||||
|
||||
cx.new_view(|cx| Example::new(language_registry, tool_registry, user_store, cx))
|
||||
});
|
||||
cx.activate(true);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
|
||||
struct Example {
|
||||
assistant_panel: View<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assistant_panel: cx.new_view(|cx| {
|
||||
AssistantPanel::new(language_registry, tool_registry, user_store, None, cx)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
|
||||
div().size_full().child(self.assistant_panel.clone())
|
||||
}
|
||||
}
|
||||
@@ -1,795 +0,0 @@
|
||||
mod assistant_settings;
|
||||
mod completion_provider;
|
||||
mod tools;
|
||||
pub mod ui;
|
||||
|
||||
use ::ui::{div, prelude::*, Color, ViewContext};
|
||||
use anyhow::{Context, Result};
|
||||
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
|
||||
use client::{proto, Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use completion_provider::*;
|
||||
use editor::Editor;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use futures::{future::join_all, StreamExt};
|
||||
use gpui::{
|
||||
list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
|
||||
FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
|
||||
use rich_text::RichText;
|
||||
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, SemanticIndex};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use ui::Composer;
|
||||
use util::{paths::EMBEDDINGS_DIR, ResultExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
pub use assistant_settings::AssistantSettings;
|
||||
|
||||
use crate::tools::{CreateBufferTool, ProjectIndexTool};
|
||||
use crate::ui::UserOrAssistant;
|
||||
|
||||
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
|
||||
pub struct Submit(SubmitMode);
|
||||
|
||||
/// There are multiple different ways to submit a model request, represented by this enum.
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
|
||||
pub enum SubmitMode {
|
||||
/// Only include the conversation.
|
||||
Simple,
|
||||
/// Send the current file as context.
|
||||
CurrentFile,
|
||||
/// Search the codebase and send relevant excerpts.
|
||||
Codebase,
|
||||
}
|
||||
|
||||
gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex]);
|
||||
gpui::impl_actions!(assistant2, [Submit]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
AssistantSettings::register(cx);
|
||||
|
||||
cx.spawn(|mut cx| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
|
||||
let semantic_index = SemanticIndex::new(
|
||||
EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
cx.update(|cx| cx.set_global(semantic_index))
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.set_global(CompletionProvider::new(CloudCompletionProvider::new(
|
||||
client,
|
||||
)));
|
||||
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn enabled(cx: &AppContext) -> bool {
|
||||
cx.is_staff()
|
||||
}
|
||||
|
||||
pub struct AssistantPanel {
|
||||
chat: View<AssistantChat>,
|
||||
width: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (app_state, project) = workspace.update(&mut cx, |workspace, _| {
|
||||
(workspace.app_state().clone(), workspace.project().clone())
|
||||
})?;
|
||||
|
||||
let user_store = app_state.user_store.clone();
|
||||
|
||||
cx.new_view(|cx| {
|
||||
// todo!("this will panic if the semantic index failed to load or has not loaded yet")
|
||||
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
|
||||
semantic_index.project_index(project.clone(), cx)
|
||||
});
|
||||
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(
|
||||
ProjectIndexTool::new(project_index.clone(), app_state.fs.clone()),
|
||||
cx,
|
||||
)
|
||||
.context("failed to register ProjectIndexTool")
|
||||
.log_err();
|
||||
tool_registry
|
||||
.register(
|
||||
CreateBufferTool::new(workspace.clone(), project.clone()),
|
||||
cx,
|
||||
)
|
||||
.context("failed to register CreateBufferTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
Self::new(
|
||||
app_state.languages.clone(),
|
||||
tool_registry,
|
||||
user_store,
|
||||
Some(project_index),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
project_index: Option<Model<ProjectIndex>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let chat = cx.new_view(|cx| {
|
||||
AssistantChat::new(
|
||||
language_registry.clone(),
|
||||
tool_registry.clone(),
|
||||
user_store,
|
||||
project_index,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self { width: None, chat }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.v_flex()
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(self.chat.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for AssistantPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"AssistantPanelv2"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> workspace::dock::DockPosition {
|
||||
// todo!("Add a setting / use assistant settings")
|
||||
DockPosition::Right
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool {
|
||||
matches!(position, DockPosition::Right)
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _: workspace::dock::DockPosition, _: &mut ViewContext<Self>) {
|
||||
// Do nothing until we have a setting for this
|
||||
}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
self.width.unwrap_or(px(400.))
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
|
||||
self.width = size;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> {
|
||||
Some(IconName::Ai)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
|
||||
Some("Assistant Panel ✨")
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn gpui::Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for AssistantPanel {}
|
||||
|
||||
impl FocusableView for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.chat.read(cx).composer_editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssistantChat {
|
||||
model: String,
|
||||
messages: Vec<ChatMessage>,
|
||||
list_state: ListState,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
composer_editor: View<Editor>,
|
||||
user_store: Model<UserStore>,
|
||||
next_message_id: MessageId,
|
||||
collapsed_messages: HashMap<MessageId, bool>,
|
||||
editing_message_id: Option<MessageId>,
|
||||
pending_completion: Option<Task<()>>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
project_index: Option<Model<ProjectIndex>>,
|
||||
}
|
||||
|
||||
impl AssistantChat {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
project_index: Option<Model<ProjectIndex>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model = CompletionProvider::get(cx).default_model();
|
||||
let view = cx.view().downgrade();
|
||||
let list_state = ListState::new(
|
||||
0,
|
||||
ListAlignment::Bottom,
|
||||
px(1024.),
|
||||
move |ix, cx: &mut WindowContext| {
|
||||
view.update(cx, |this, cx| this.render_message(ix, cx))
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
model,
|
||||
messages: Vec::new(),
|
||||
composer_editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_placeholder_text("Type a message to the assistant", cx);
|
||||
editor
|
||||
}),
|
||||
list_state,
|
||||
user_store,
|
||||
language_registry,
|
||||
project_index,
|
||||
next_message_id: MessageId(0),
|
||||
editing_message_id: None,
|
||||
collapsed_messages: HashMap::default(),
|
||||
pending_completion: None,
|
||||
tool_registry,
|
||||
}
|
||||
}
|
||||
|
||||
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
|
||||
self.messages.iter().find_map(|message| match message {
|
||||
ChatMessage::User(message) => message
|
||||
.body
|
||||
.focus_handle(cx)
|
||||
.contains_focused(cx)
|
||||
.then_some(message.id),
|
||||
ChatMessage::Assistant(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
// If we're currently editing a message, cancel the edit.
|
||||
self.editing_message_id.take();
|
||||
|
||||
if self.pending_completion.take().is_none() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(ChatMessage::Assistant(message)) = self.messages.last() {
|
||||
if message.body.text.is_empty() {
|
||||
self.pop_message(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
|
||||
// Don't allow multiple concurrent completions.
|
||||
if self.pending_completion.is_some() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(focused_message_id) = self.focused_message_id(cx) {
|
||||
self.truncate_messages(focused_message_id, cx);
|
||||
} else if self.composer_editor.focus_handle(cx).is_focused(cx) {
|
||||
let message = self.composer_editor.update(cx, |composer_editor, cx| {
|
||||
let text = composer_editor.text(cx);
|
||||
let id = self.next_message_id.post_inc();
|
||||
let body = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_text(text, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
composer_editor.clear(cx);
|
||||
ChatMessage::User(UserMessage { id, body })
|
||||
});
|
||||
self.push_message(message, cx);
|
||||
} else {
|
||||
log::error!("unexpected state: no user message editor is focused.");
|
||||
return;
|
||||
}
|
||||
|
||||
let mode = *mode;
|
||||
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
|
||||
Self::request_completion(
|
||||
this.clone(),
|
||||
mode,
|
||||
MAX_COMPLETION_CALLS_PER_SUBMISSION,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let composer_focus_handle = this.composer_editor.focus_handle(cx);
|
||||
cx.focus(&composer_focus_handle);
|
||||
this.pending_completion = None;
|
||||
})
|
||||
.context("Failed to push new user message")
|
||||
.log_err();
|
||||
}));
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
self.pending_completion.is_none()
|
||||
}
|
||||
|
||||
fn debug_project_index(&mut self, _: &DebugProjectIndex, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = &self.project_index {
|
||||
index.update(cx, |project_index, cx| {
|
||||
project_index.debug(cx).detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_completion(
|
||||
this: WeakView<Self>,
|
||||
mode: SubmitMode,
|
||||
limit: usize,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let mut call_count = 0;
|
||||
loop {
|
||||
let complete = async {
|
||||
let completion = this.update(cx, |this, cx| {
|
||||
this.push_new_assistant_message(cx);
|
||||
|
||||
let definitions = if call_count < limit
|
||||
&& matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
|
||||
{
|
||||
this.tool_registry.definitions()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
call_count += 1;
|
||||
|
||||
let messages = this.completion_messages(cx);
|
||||
|
||||
CompletionProvider::get(cx).complete(
|
||||
this.model.clone(),
|
||||
messages,
|
||||
Vec::new(),
|
||||
1.0,
|
||||
definitions,
|
||||
)
|
||||
});
|
||||
|
||||
let mut stream = completion?.await?;
|
||||
let mut body = String::new();
|
||||
while let Some(delta) = stream.next().await {
|
||||
let delta = delta?;
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(ChatMessage::Assistant(AssistantMessage {
|
||||
body: message_body,
|
||||
tool_calls: message_tool_calls,
|
||||
..
|
||||
})) = this.messages.last_mut()
|
||||
{
|
||||
if let Some(content) = &delta.content {
|
||||
body.push_str(content);
|
||||
}
|
||||
|
||||
for tool_call in delta.tool_calls {
|
||||
let index = tool_call.index as usize;
|
||||
if index >= message_tool_calls.len() {
|
||||
message_tool_calls.resize_with(index + 1, Default::default);
|
||||
}
|
||||
let call = &mut message_tool_calls[index];
|
||||
|
||||
if let Some(id) = &tool_call.id {
|
||||
call.id.push_str(id);
|
||||
}
|
||||
|
||||
match tool_call.variant {
|
||||
Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
|
||||
if let Some(name) = &tool_call.name {
|
||||
call.name.push_str(name);
|
||||
}
|
||||
if let Some(arguments) = &tool_call.arguments {
|
||||
call.arguments.push_str(arguments);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
*message_body =
|
||||
RichText::new(body.clone(), &[], &this.language_registry);
|
||||
cx.notify();
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
let mut tool_tasks = Vec::new();
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(ChatMessage::Assistant(AssistantMessage {
|
||||
error: message_error,
|
||||
tool_calls,
|
||||
..
|
||||
})) = this.messages.last_mut()
|
||||
{
|
||||
if let Err(error) = complete {
|
||||
message_error.replace(SharedString::from(error.to_string()));
|
||||
cx.notify();
|
||||
} else {
|
||||
for tool_call in tool_calls.iter() {
|
||||
tool_tasks.push(this.tool_registry.call(tool_call, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
if tool_tasks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tools = join_all(tool_tasks.into_iter()).await;
|
||||
// If the WindowContext went away for any tool's view we don't include it
|
||||
// especially since the below call would fail for the same reason.
|
||||
let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) =
|
||||
this.messages.last_mut()
|
||||
{
|
||||
*tool_calls = tools;
|
||||
cx.notify();
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let message = ChatMessage::Assistant(AssistantMessage {
|
||||
id: self.next_message_id.post_inc(),
|
||||
body: RichText::default(),
|
||||
tool_calls: Vec::new(),
|
||||
error: None,
|
||||
});
|
||||
self.push_message(message, cx);
|
||||
}
|
||||
|
||||
fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext<Self>) {
|
||||
let old_len = self.messages.len();
|
||||
let focus_handle = Some(message.focus_handle(cx));
|
||||
self.messages.push(message);
|
||||
self.list_state
|
||||
.splice_focusable(old_len..old_len, focus_handle);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn pop_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.messages.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.messages.pop();
|
||||
self.list_state
|
||||
.splice(self.messages.len()..self.messages.len() + 1, 0);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.messages.iter().position(|message| match message {
|
||||
ChatMessage::User(message) => message.id == last_message_id,
|
||||
ChatMessage::Assistant(message) => message.id == last_message_id,
|
||||
}) {
|
||||
self.list_state.splice(index + 1..self.messages.len(), 0);
|
||||
self.messages.truncate(index + 1);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_message_collapsed(&self, id: &MessageId) -> bool {
|
||||
self.collapsed_messages.get(id).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_message_collapsed(&mut self, id: MessageId) {
|
||||
let entry = self.collapsed_messages.entry(id).or_insert(false);
|
||||
*entry = !*entry;
|
||||
}
|
||||
|
||||
fn render_error(
|
||||
&self,
|
||||
error: Option<SharedString>,
|
||||
_ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
if let Some(error) = error {
|
||||
div()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.neg_mx_1()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(theme.status().error_border)
|
||||
// .bg(theme.status().error_background)
|
||||
.text_color(theme.status().error)
|
||||
.child(error.clone())
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
let is_last = ix == self.messages.len() - 1;
|
||||
|
||||
match &self.messages[ix] {
|
||||
ChatMessage::User(UserMessage { id, body }) => div()
|
||||
.id(SharedString::from(format!("message-{}-container", id.0)))
|
||||
.when(!is_last, |element| element.mb_2())
|
||||
.map(|element| {
|
||||
if self.editing_message_id.as_ref() == Some(id) {
|
||||
element.child(Composer::new(
|
||||
body.clone(),
|
||||
self.user_store.read(cx).current_user(),
|
||||
self.can_submit(),
|
||||
self.tool_registry.clone(),
|
||||
crate::ui::ModelSelector::new(
|
||||
cx.view().downgrade(),
|
||||
self.model.clone(),
|
||||
)
|
||||
.into_any_element(),
|
||||
))
|
||||
} else {
|
||||
element
|
||||
.on_click(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, event: &ClickEvent, _cx| {
|
||||
if event.up.click_count == 2 {
|
||||
assistant_chat.editing_message_id = Some(id);
|
||||
}
|
||||
}
|
||||
}))
|
||||
.child(crate::ui::ChatMessage::new(
|
||||
*id,
|
||||
UserOrAssistant::User(self.user_store.read(cx).current_user()),
|
||||
Some(
|
||||
RichText::new(
|
||||
body.read(cx).text(cx),
|
||||
&[],
|
||||
&self.language_registry,
|
||||
)
|
||||
.element(ElementId::from(id.0), cx),
|
||||
),
|
||||
self.is_message_collapsed(id),
|
||||
Box::new(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, _event, _cx| {
|
||||
assistant_chat.toggle_message_collapsed(id)
|
||||
}
|
||||
})),
|
||||
))
|
||||
}
|
||||
})
|
||||
.into_any(),
|
||||
ChatMessage::Assistant(AssistantMessage {
|
||||
id,
|
||||
body,
|
||||
error,
|
||||
tool_calls,
|
||||
..
|
||||
}) => {
|
||||
let assistant_body = if body.text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
div()
|
||||
.p_2()
|
||||
.child(body.element(ElementId::from(id.0), cx))
|
||||
.into_any_element(),
|
||||
)
|
||||
};
|
||||
|
||||
div()
|
||||
.when(!is_last, |element| element.mb_2())
|
||||
.child(crate::ui::ChatMessage::new(
|
||||
*id,
|
||||
UserOrAssistant::Assistant,
|
||||
assistant_body,
|
||||
self.is_message_collapsed(id),
|
||||
Box::new(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, _event, _cx| {
|
||||
assistant_chat.toggle_message_collapsed(id)
|
||||
}
|
||||
})),
|
||||
))
|
||||
// TODO: Should the errors and tool calls get passed into `ChatMessage`?
|
||||
.child(self.render_error(error.clone(), ix, cx))
|
||||
.children(tool_calls.iter().map(|tool_call| {
|
||||
let result = &tool_call.result;
|
||||
let name = tool_call.name.clone();
|
||||
match result {
|
||||
Some(result) => {
|
||||
div().p_2().child(result.into_any_element(&name)).into_any()
|
||||
}
|
||||
None => div()
|
||||
.p_2()
|
||||
.child(Label::new(name).color(Color::Modified))
|
||||
.child("Running...")
|
||||
.into_any(),
|
||||
}
|
||||
}))
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_messages(&self, cx: &mut WindowContext) -> Vec<CompletionMessage> {
|
||||
let mut completion_messages = Vec::new();
|
||||
|
||||
for message in &self.messages {
|
||||
match message {
|
||||
ChatMessage::User(UserMessage { body, .. }) => {
|
||||
// When we re-introduce contexts like active file, we'll inject them here instead of relying on the model to request them
|
||||
// contexts.iter().for_each(|context| {
|
||||
// completion_messages.extend(context.completion_messages(cx))
|
||||
// });
|
||||
|
||||
// Show user's message last so that the assistant is grounded in the user's request
|
||||
completion_messages.push(CompletionMessage::User {
|
||||
content: body.read(cx).text(cx),
|
||||
});
|
||||
}
|
||||
ChatMessage::Assistant(AssistantMessage {
|
||||
body, tool_calls, ..
|
||||
}) => {
|
||||
// In no case do we want to send an empty message. This shouldn't happen, but we might as well
|
||||
// not break the Chat API if it does.
|
||||
if body.text.is_empty() && tool_calls.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tool_calls_from_assistant = tool_calls
|
||||
.iter()
|
||||
.map(|tool_call| ToolCall {
|
||||
content: ToolCallContent::Function {
|
||||
function: FunctionContent {
|
||||
name: tool_call.name.clone(),
|
||||
arguments: tool_call.arguments.clone(),
|
||||
},
|
||||
},
|
||||
id: tool_call.id.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
completion_messages.push(CompletionMessage::Assistant {
|
||||
content: Some(body.text.to_string()),
|
||||
tool_calls: tool_calls_from_assistant,
|
||||
});
|
||||
|
||||
for tool_call in tool_calls {
|
||||
// todo!(): we should not be sending when the tool is still running / has no result
|
||||
// For now I'm going to have to assume we send an empty string because otherwise
|
||||
// the Chat API will break -- there is a required message for every tool call by ID
|
||||
let content = match &tool_call.result {
|
||||
Some(result) => result.format(&tool_call.name),
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
completion_messages.push(CompletionMessage::Tool {
|
||||
content,
|
||||
tool_call_id: tool_call.id.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion_messages
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantChat {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.v_flex()
|
||||
.key_context("AssistantChat")
|
||||
.on_action(cx.listener(Self::submit))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::debug_project_index))
|
||||
.text_color(Color::Default.color(cx))
|
||||
.child(list(self.list_state.clone()).flex_1())
|
||||
.child(Composer::new(
|
||||
self.composer_editor.clone(),
|
||||
self.user_store.read(cx).current_user(),
|
||||
self.can_submit(),
|
||||
self.tool_registry.clone(),
|
||||
crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
|
||||
.into_any_element(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub struct MessageId(usize);
|
||||
|
||||
impl MessageId {
|
||||
fn post_inc(&mut self) -> Self {
|
||||
let id = *self;
|
||||
self.0 += 1;
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatMessage {
|
||||
User(UserMessage),
|
||||
Assistant(AssistantMessage),
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
|
||||
match self {
|
||||
ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
|
||||
ChatMessage::Assistant(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserMessage {
|
||||
id: MessageId,
|
||||
body: View<Editor>,
|
||||
}
|
||||
|
||||
struct AssistantMessage {
|
||||
id: MessageId,
|
||||
body: RichText,
|
||||
tool_calls: Vec<ToolFunctionCall>,
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant_v2");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(sources.json_merge().unwrap_or_else(|_| Default::default()))
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::ToolFunctionDefinition;
|
||||
use client::{proto, Client};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, Global};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use open_ai::RequestMessage as CompletionMessage;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
|
||||
|
||||
impl CompletionProvider {
|
||||
pub fn get(cx: &AppContext) -> &Self {
|
||||
cx.global::<CompletionProvider>()
|
||||
}
|
||||
|
||||
pub fn new(backend: impl CompletionProviderBackend) -> Self {
|
||||
Self(Arc::new(backend))
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> String {
|
||||
self.0.default_model()
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> Vec<String> {
|
||||
self.0.available_models()
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
model: String,
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
|
||||
{
|
||||
self.0.complete(model, messages, stop, temperature, tools)
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for CompletionProvider {}
|
||||
|
||||
pub trait CompletionProviderBackend: 'static {
|
||||
fn default_model(&self) -> String;
|
||||
fn available_models(&self) -> Vec<String>;
|
||||
fn complete(
|
||||
&self,
|
||||
model: String,
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>;
|
||||
}
|
||||
|
||||
pub struct CloudCompletionProvider {
|
||||
client: Arc<Client>,
|
||||
}
|
||||
|
||||
impl CloudCompletionProvider {
|
||||
pub fn new(client: Arc<Client>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProviderBackend for CloudCompletionProvider {
|
||||
fn default_model(&self) -> String {
|
||||
"gpt-4-turbo".into()
|
||||
}
|
||||
|
||||
fn available_models(&self) -> Vec<String> {
|
||||
vec!["gpt-4-turbo".into(), "gpt-4".into(), "gpt-3.5-turbo".into()]
|
||||
}
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
model: String,
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
|
||||
{
|
||||
let client = self.client.clone();
|
||||
let tools: Vec<proto::ChatCompletionTool> = tools
|
||||
.iter()
|
||||
.filter_map(|tool| {
|
||||
Some(proto::ChatCompletionTool {
|
||||
variant: Some(proto::chat_completion_tool::Variant::Function(
|
||||
proto::chat_completion_tool::FunctionObject {
|
||||
name: tool.name.clone(),
|
||||
description: Some(tool.description.clone()),
|
||||
parameters: Some(serde_json::to_string(&tool.parameters).ok()?),
|
||||
},
|
||||
)),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tool_choice = match tools.is_empty() {
|
||||
true => None,
|
||||
false => Some("auto".into()),
|
||||
};
|
||||
|
||||
async move {
|
||||
let stream = client
|
||||
.request_stream(proto::CompleteWithLanguageModel {
|
||||
model,
|
||||
messages: messages
|
||||
.into_iter()
|
||||
.map(|message| match message {
|
||||
CompletionMessage::Assistant {
|
||||
content,
|
||||
tool_calls,
|
||||
} => proto::LanguageModelRequestMessage {
|
||||
role: proto::LanguageModelRole::LanguageModelAssistant as i32,
|
||||
content: content.unwrap_or_default(),
|
||||
tool_call_id: None,
|
||||
tool_calls: tool_calls
|
||||
.into_iter()
|
||||
.map(|tool_call| match tool_call.content {
|
||||
open_ai::ToolCallContent::Function { function } => {
|
||||
proto::ToolCall {
|
||||
id: tool_call.id,
|
||||
variant: Some(proto::tool_call::Variant::Function(
|
||||
proto::tool_call::FunctionCall {
|
||||
name: function.name,
|
||||
arguments: function.arguments,
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
CompletionMessage::User { content } => {
|
||||
proto::LanguageModelRequestMessage {
|
||||
role: proto::LanguageModelRole::LanguageModelUser as i32,
|
||||
content,
|
||||
tool_call_id: None,
|
||||
tool_calls: Vec::new(),
|
||||
}
|
||||
}
|
||||
CompletionMessage::System { content } => {
|
||||
proto::LanguageModelRequestMessage {
|
||||
role: proto::LanguageModelRole::LanguageModelSystem as i32,
|
||||
content,
|
||||
tool_calls: Vec::new(),
|
||||
tool_call_id: None,
|
||||
}
|
||||
}
|
||||
CompletionMessage::Tool {
|
||||
content,
|
||||
tool_call_id,
|
||||
} => proto::LanguageModelRequestMessage {
|
||||
role: proto::LanguageModelRole::LanguageModelTool as i32,
|
||||
content,
|
||||
tool_call_id: Some(tool_call_id),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
stop,
|
||||
temperature,
|
||||
tool_choice,
|
||||
tools,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(stream
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mod create_buffer;
|
||||
mod project_index;
|
||||
|
||||
pub use create_buffer::*;
|
||||
pub use project_index::*;
|
||||
@@ -1,111 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::LanguageModelTool;
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, Model, Task, View, WeakView};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct CreateBufferTool {
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
}
|
||||
|
||||
impl CreateBufferTool {
|
||||
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
|
||||
Self { workspace, project }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct CreateBufferInput {
|
||||
/// The contents of the buffer.
|
||||
text: String,
|
||||
|
||||
/// The name of the language to use for the buffer.
|
||||
///
|
||||
/// This should be a human-readable name, like "Rust", "JavaScript", or "Python".
|
||||
language: String,
|
||||
}
|
||||
|
||||
pub struct CreateBufferOutput {}
|
||||
|
||||
impl LanguageModelTool for CreateBufferTool {
|
||||
type Input = CreateBufferInput;
|
||||
type Output = CreateBufferOutput;
|
||||
type View = CreateBufferView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"create_buffer".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Create a new buffer in the current codebase".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
|
||||
cx.spawn({
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let text = input.text.clone();
|
||||
let language_name = input.language.clone();
|
||||
|mut cx| async move {
|
||||
let language = cx
|
||||
.update(|cx| {
|
||||
project
|
||||
.read(cx)
|
||||
.languages()
|
||||
.language_for_name(&language_name)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let buffer = cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.create_buffer(&text, Some(language), cx)
|
||||
})
|
||||
})??;
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(
|
||||
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project), cx)),
|
||||
),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Ok(CreateBufferOutput {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
match output {
|
||||
Ok(_) => format!("Created a new {} buffer", input.language),
|
||||
Err(err) => format!("Failed to create buffer: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
_output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| CreateBufferView {})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateBufferView {}
|
||||
|
||||
impl Render for CreateBufferView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().child("Opening a buffer")
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::LanguageModelTool;
|
||||
use gpui::{prelude::*, AnyView, Model, Task};
|
||||
use project::Fs;
|
||||
use schemars::JsonSchema;
|
||||
use semantic_index::{ProjectIndex, Status};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString,
|
||||
WindowContext,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
const DEFAULT_SEARCH_LIMIT: usize = 20;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodebaseExcerpt {
|
||||
path: SharedString,
|
||||
text: SharedString,
|
||||
score: f32,
|
||||
element_id: ElementId,
|
||||
expanded: bool,
|
||||
}
|
||||
|
||||
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
|
||||
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct CodebaseQuery {
|
||||
/// Semantic search query
|
||||
query: String,
|
||||
/// Maximum number of results to return, defaults to 20
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct ProjectIndexView {
|
||||
input: CodebaseQuery,
|
||||
output: Result<ProjectIndexOutput>,
|
||||
}
|
||||
|
||||
impl ProjectIndexView {
|
||||
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
|
||||
if let Ok(output) = &mut self.output {
|
||||
if let Some(excerpt) = output
|
||||
.excerpts
|
||||
.iter_mut()
|
||||
.find(|excerpt| excerpt.element_id == element_id)
|
||||
{
|
||||
excerpt.expanded = !excerpt.expanded;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectIndexView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let query = self.input.query.clone();
|
||||
|
||||
let result = &self.output;
|
||||
|
||||
let output = match result {
|
||||
Err(err) => {
|
||||
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
|
||||
}
|
||||
Ok(output) => output,
|
||||
};
|
||||
|
||||
div()
|
||||
.v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(Label::new("Query: ").color(Color::Modified))
|
||||
.child(Label::new(query).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
.children(output.excerpts.iter().map(|excerpt| {
|
||||
let element_id = excerpt.element_id.clone();
|
||||
let expanded = excerpt.expanded;
|
||||
|
||||
CollapsibleContainer::new(element_id.clone(), expanded)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::File).color(Color::Muted))
|
||||
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_expanded(element_id.clone(), cx);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(excerpt.text.clone()),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectIndexTool {
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
pub struct ProjectIndexOutput {
|
||||
excerpts: Vec<CodebaseExcerpt>,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
impl ProjectIndexTool {
|
||||
pub fn new(project_index: Model<ProjectIndex>, fs: Arc<dyn Fs>) -> Self {
|
||||
// Listen for project index status and update the ProjectIndexTool directly
|
||||
|
||||
// TODO: setup a better description based on the user's current codebase.
|
||||
Self { project_index, fs }
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for ProjectIndexTool {
|
||||
type Input = CodebaseQuery;
|
||||
type Output = ProjectIndexOutput;
|
||||
type View = ProjectIndexView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"query_codebase".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of chunks and an embedding of the query".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, query: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
|
||||
let project_index = self.project_index.read(cx);
|
||||
|
||||
let status = project_index.status();
|
||||
let results = project_index.search(
|
||||
query.query.as_str(),
|
||||
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||
cx,
|
||||
);
|
||||
|
||||
let fs = self.fs.clone();
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
let results = results.await;
|
||||
|
||||
let excerpts = results.into_iter().map(|result| {
|
||||
let abs_path = result
|
||||
.worktree
|
||||
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
|
||||
let fs = fs.clone();
|
||||
|
||||
async move {
|
||||
let path = result.path.clone();
|
||||
let text = fs.load(&abs_path?).await?;
|
||||
|
||||
let mut start = result.range.start;
|
||||
let mut end = result.range.end.min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
anyhow::Ok(CodebaseExcerpt {
|
||||
element_id: ElementId::Name(nanoid::nanoid!().into()),
|
||||
expanded: false,
|
||||
path: path.to_string_lossy().to_string().into(),
|
||||
text: SharedString::from(text[start..end].to_string()),
|
||||
score: result.score,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let excerpts = futures::future::join_all(excerpts)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect();
|
||||
anyhow::Ok(ProjectIndexOutput { excerpts, status })
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| ProjectIndexView { input, output })
|
||||
}
|
||||
|
||||
fn status_view(&self, cx: &mut WindowContext) -> Option<AnyView> {
|
||||
Some(
|
||||
cx.new_view(|cx| ProjectIndexStatusView::new(self.project_index.clone(), cx))
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
match &output {
|
||||
Ok(output) => {
|
||||
let mut body = "Semantic search results:\n".to_string();
|
||||
|
||||
if output.status != Status::Idle {
|
||||
body.push_str("Still indexing. Results may be incomplete.\n");
|
||||
}
|
||||
|
||||
if output.excerpts.is_empty() {
|
||||
body.push_str("No results found");
|
||||
return body;
|
||||
}
|
||||
|
||||
for excerpt in &output.excerpts {
|
||||
body.push_str("Excerpt from ");
|
||||
body.push_str(excerpt.path.as_ref());
|
||||
body.push_str(", score ");
|
||||
body.push_str(&excerpt.score.to_string());
|
||||
body.push_str(":\n");
|
||||
body.push_str("~~~\n");
|
||||
body.push_str(excerpt.text.as_ref());
|
||||
body.push_str("~~~\n");
|
||||
}
|
||||
body
|
||||
}
|
||||
Err(err) => format!("Error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProjectIndexStatusView {
|
||||
project_index: Model<ProjectIndex>,
|
||||
}
|
||||
|
||||
impl ProjectIndexStatusView {
|
||||
pub fn new(project_index: Model<ProjectIndex>, cx: &mut ViewContext<Self>) -> Self {
|
||||
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
Self { project_index }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectIndexStatusView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let status = self.project_index.read(cx).status();
|
||||
|
||||
h_flex().gap_2().map(|element| match status {
|
||||
Status::Idle => element.child(Label::new("Project index ready")),
|
||||
Status::Loading => element.child(Label::new("Project index loading...")),
|
||||
Status::Scanning { remaining_count } => element.child(Label::new(format!(
|
||||
"Project index scanning: {remaining_count} remaining..."
|
||||
))),
|
||||
})
|
||||
}
|
||||
}
|
||||