Compare commits
206 Commits
store-reso
...
markdown-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db28a0ecde | ||
|
|
2223e6e5e1 | ||
|
|
f3140f54d8 | ||
|
|
72afe684b8 | ||
|
|
59dc6cf523 | ||
|
|
b88daae67b | ||
|
|
f32ffcf5bb | ||
|
|
95a047c11b | ||
|
|
dbe41823d9 | ||
|
|
7c40824783 | ||
|
|
4e12f0580a | ||
|
|
f795ce9623 | ||
|
|
995b40f149 | ||
|
|
89e46396f6 | ||
|
|
3987d0d731 | ||
|
|
6cb758a1cd | ||
|
|
0cb3a6ed0e | ||
|
|
2300f40cd9 | ||
|
|
dacd919e27 | ||
|
|
740ba7817b | ||
|
|
380679fcc2 | ||
|
|
89a56968f6 | ||
|
|
5f6b200d8d | ||
|
|
4d5415273e | ||
|
|
bf569d720e | ||
|
|
28849dd2a8 | ||
|
|
c2cd84a749 | ||
|
|
d609931e1c | ||
|
|
fd71801346 | ||
|
|
c1de606581 | ||
|
|
57a45d80ad | ||
|
|
5f29f214c3 | ||
|
|
4bf59393ec | ||
|
|
aea6fa0c09 | ||
|
|
4137d1adb9 | ||
|
|
1903a29cca | ||
|
|
69c761f5a5 | ||
|
|
0306bdc695 | ||
|
|
de55bd8307 | ||
|
|
a593a04da4 | ||
|
|
74f265e5cf | ||
|
|
f9d5de834a | ||
|
|
eadb107339 | ||
|
|
94faf9dd56 | ||
|
|
73f546ea5f | ||
|
|
eb2c0b33df | ||
|
|
3458687300 | ||
|
|
e76589107d | ||
|
|
ae85ecba2d | ||
|
|
0acd98a07e | ||
|
|
4a96db026c | ||
|
|
301a8900a5 | ||
|
|
f30944543e | ||
|
|
6cba467a4e | ||
|
|
cacec06db6 | ||
|
|
3ac119ac4e | ||
|
|
b12a508ed9 | ||
|
|
1739de59d4 | ||
|
|
4aa47a9063 | ||
|
|
fe30a03921 | ||
|
|
38900c2321 | ||
|
|
6927512e34 | ||
|
|
4342a93d22 | ||
|
|
28640ac076 | ||
|
|
c2c968f2de | ||
|
|
a4584c9d13 | ||
|
|
e9e260776b | ||
|
|
461ab24a06 | ||
|
|
04ff9f060c | ||
|
|
66ba9d5b4b | ||
|
|
e803815b16 | ||
|
|
34ed48e14b | ||
|
|
0c8e5550e7 | ||
|
|
cff9ae0bbc | ||
|
|
d0bafce86b | ||
|
|
4564da2875 | ||
|
|
c021ee60d6 | ||
|
|
6736806924 | ||
|
|
ce6782f4c8 | ||
|
|
e865b6c524 | ||
|
|
4e720be41c | ||
|
|
f702575255 | ||
|
|
d75d34576a | ||
|
|
57e4540759 | ||
|
|
597e5f8304 | ||
|
|
64708527e7 | ||
|
|
6dbe2ef10c | ||
|
|
884748038e | ||
|
|
8f1ec3d11b | ||
|
|
fdc17c57d7 | ||
|
|
9999c31859 | ||
|
|
7d67bb4cf6 | ||
|
|
968ffaa3fd | ||
|
|
7e418cc8af | ||
|
|
f059b6a24b | ||
|
|
3901d46101 | ||
|
|
321fd19763 | ||
|
|
cc5daa22bd | ||
|
|
2b9250843c | ||
|
|
e7b0047562 | ||
|
|
9ee1aba80a | ||
|
|
91a565f5fa | ||
|
|
a02684b2f7 | ||
|
|
bd02b35ba9 | ||
|
|
28142be5e9 | ||
|
|
389422cbf3 | ||
|
|
93533ed235 | ||
|
|
385c447bbe | ||
|
|
b83f104f6e | ||
|
|
08b214dfb9 | ||
|
|
aa58cab766 | ||
|
|
5b0fa6e585 | ||
|
|
e85848a695 | ||
|
|
20bffaf93f | ||
|
|
3dcb94c204 | ||
|
|
0395d1b037 | ||
|
|
628b96f297 | ||
|
|
2a23db6e05 | ||
|
|
3a0408953d | ||
|
|
9adc3b4e82 | ||
|
|
f30de4852a | ||
|
|
2177e833d8 | ||
|
|
8a9c53524a | ||
|
|
43f0ea759b | ||
|
|
984bb192ba | ||
|
|
5766afe710 | ||
|
|
9833756224 | ||
|
|
1cfcdfa7ac | ||
|
|
c9f2c2792c | ||
|
|
8240a52a39 | ||
|
|
c28f5b11f8 | ||
|
|
96854c68ea | ||
|
|
becc36380f | ||
|
|
1a0a8a9559 | ||
|
|
2fd210bc9a | ||
|
|
23321be2ce | ||
|
|
659b1c9dcf | ||
|
|
cb8028c092 | ||
|
|
ca76948044 | ||
|
|
852fb51528 | ||
|
|
d489f96aef | ||
|
|
b4659bb44e | ||
|
|
d5f2bca382 | ||
|
|
114c462143 | ||
|
|
933c11a9b2 | ||
|
|
14ea4621ab | ||
|
|
477c6e6833 | ||
|
|
6c470748ac | ||
|
|
e0245b3f30 | ||
|
|
9211e699ee | ||
|
|
0663bf2a53 | ||
|
|
9d95da56c3 | ||
|
|
5ee5a1a51e | ||
|
|
72613b7668 | ||
|
|
f74f670865 | ||
|
|
af34953bc3 | ||
|
|
b102a40e04 | ||
|
|
790fdcf737 | ||
|
|
2868b67286 | ||
|
|
614b3b979b | ||
|
|
4c7b48b35d | ||
|
|
6b2f1cc543 | ||
|
|
f62ccf9c8a | ||
|
|
841d3221b3 | ||
|
|
02447a8552 | ||
|
|
c16dfc1a39 | ||
|
|
268ac4c047 | ||
|
|
571c7d4f66 | ||
|
|
5ff49db92f | ||
|
|
395e25be25 | ||
|
|
74223c1b00 | ||
|
|
0b373d43dc | ||
|
|
75c545aa1e | ||
|
|
6ab4b46984 | ||
|
|
ebaa270baf | ||
|
|
7285cdb955 | ||
|
|
e062f30d9e | ||
|
|
37a59d6b2e | ||
|
|
a03770837e | ||
|
|
0e62b6dddd | ||
|
|
95ace03706 | ||
|
|
49ed932c1f | ||
|
|
33e84da657 | ||
|
|
536d7e5355 | ||
|
|
cbba44900d | ||
|
|
335b112abd | ||
|
|
33bed8d680 | ||
|
|
6d4a5f9ad2 | ||
|
|
427c2017c3 | ||
|
|
ebca6a8f3d | ||
|
|
29c9f0f6a1 | ||
|
|
e076f55d78 | ||
|
|
e31f44450e | ||
|
|
e0761db62d | ||
|
|
8c342ef706 | ||
|
|
7e67753d51 | ||
|
|
1475a7000f | ||
|
|
41fd9189e3 | ||
|
|
973498e075 | ||
|
|
b63394f4bd | ||
|
|
743165fa6c | ||
|
|
e03968f538 | ||
|
|
3c57a4071c | ||
|
|
ad6a07e574 | ||
|
|
c2668bc953 | ||
|
|
705a06c3dd |
@@ -3,15 +3,6 @@ export default {
|
||||
const url = new URL(request.url);
|
||||
url.hostname = "docs-anw.pages.dev";
|
||||
|
||||
// These pages were removed, but may still be served due to Cloudflare's
|
||||
// [asset retention](https://developers.cloudflare.com/pages/configuration/serving-pages/#asset-retention).
|
||||
if (
|
||||
url.pathname === "/docs/assistant/context-servers" ||
|
||||
url.pathname === "/docs/assistant/model-context-protocol"
|
||||
) {
|
||||
return await fetch("https://zed.dev/404");
|
||||
}
|
||||
|
||||
let res = await fetch(url, request);
|
||||
|
||||
if (res.status === 404) {
|
||||
|
||||
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -245,6 +245,7 @@ jobs:
|
||||
# 25 was chosen arbitrarily.
|
||||
fetch-depth: 25
|
||||
clean: false
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
@@ -261,6 +262,9 @@ jobs:
|
||||
mkdir -p target/
|
||||
# Ignore any errors that occur while drafting release notes to not fail the build.
|
||||
script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true
|
||||
script/create-draft-release target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
@@ -268,18 +272,12 @@ jobs:
|
||||
- name: Create macOS app bundle
|
||||
run: script/bundle-mac
|
||||
|
||||
- name: Rename single-architecture binaries
|
||||
- name: Rename binaries
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
run: |
|
||||
mv target/aarch64-apple-darwin/release/Zed.dmg target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- name: Upload app bundle (universal) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
@@ -305,8 +303,6 @@ jobs:
|
||||
target/zed-remote-server-macos-aarch64.gz
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
body_path: target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -402,18 +398,15 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
auto-publish-release:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux bundle
|
||||
auto-release-preview:
|
||||
name: Auto release preview
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
|
||||
needs: [bundle-mac, bundle-linux, bundle-linux-aarch64]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
|
||||
needs: [bundle-mac, bundle-linux-aarch64, bundle-linux]
|
||||
- bundle
|
||||
steps:
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
with:
|
||||
draft: false
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
- name: gh release
|
||||
run: gh release edit $GITHUB_REF_NAME --draft=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3
|
||||
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: true
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3
|
||||
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: true
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/.direnv
|
||||
.envrc
|
||||
.idea
|
||||
**/target
|
||||
**/cargo-target
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"languages": {
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
"formatter": "prettier",
|
||||
"format_on_save": "on"
|
||||
},
|
||||
"TOML": {
|
||||
"formatter": "prettier",
|
||||
|
||||
595
Cargo.lock
generated
595
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
50
Cargo.toml
@@ -5,10 +5,13 @@ members = [
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_tool",
|
||||
"crates/assistant_tools",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update_ui",
|
||||
"crates/breadcrumbs",
|
||||
"crates/call",
|
||||
"crates/channel",
|
||||
@@ -20,7 +23,8 @@ members = [
|
||||
"crates/collections",
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/context_servers",
|
||||
"crates/context_server",
|
||||
"crates/context_server_settings",
|
||||
"crates/copilot",
|
||||
"crates/db",
|
||||
"crates/diagnostics",
|
||||
@@ -49,11 +53,15 @@ members = [
|
||||
"crates/http_client",
|
||||
"crates/image_viewer",
|
||||
"crates/indexed_docs",
|
||||
"crates/inline_completion",
|
||||
"crates/inline_completion_button",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
"crates/language_model",
|
||||
"crates/language_model_selector",
|
||||
"crates/language_models",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/languages",
|
||||
@@ -78,7 +86,6 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/proto",
|
||||
"crates/quick_action_bar",
|
||||
"crates/recent_projects",
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
@@ -114,6 +121,7 @@ members = [
|
||||
"crates/terminal_view",
|
||||
"crates/text",
|
||||
"crates/theme",
|
||||
"crates/theme_extension",
|
||||
"crates/theme_importer",
|
||||
"crates/theme_selector",
|
||||
"crates/time_format",
|
||||
@@ -126,6 +134,7 @@ members = [
|
||||
"crates/util",
|
||||
"crates/vcs_menu",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
@@ -181,10 +190,13 @@ ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant2 = { path = "crates/assistant2" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tool = { path = "crates/assistant_tool" }
|
||||
assistant_tools = { path = "crates/assistant_tools" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
auto_update_ui = { path = "crates/auto_update_ui" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
call = { path = "crates/call" }
|
||||
channel = { path = "crates/channel" }
|
||||
@@ -196,7 +208,8 @@ collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
context_servers = { path = "crates/context_servers" }
|
||||
context_server = { path = "crates/context_server" }
|
||||
context_server_settings = { path = "crates/context_server_settings" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
db = { path = "crates/db" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
@@ -215,17 +228,23 @@ git = { path = "crates/git" }
|
||||
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
|
||||
gpui = { path = "crates/gpui", default-features = false, features = [
|
||||
"http_client",
|
||||
] }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
indexed_docs = { path = "crates/indexed_docs" }
|
||||
inline_completion = { path = "crates/inline_completion" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
language_model_selector = { path = "crates/language_model_selector" }
|
||||
language_models = { path = "crates/language_models" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
@@ -252,7 +271,6 @@ project = { path = "crates/project" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
proto = { path = "crates/proto" }
|
||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
@@ -287,6 +305,7 @@ terminal = { path = "crates/terminal" }
|
||||
terminal_view = { path = "crates/terminal_view" }
|
||||
text = { path = "crates/text" }
|
||||
theme = { path = "crates/theme" }
|
||||
theme_extension = { path = "crates/theme_extension" }
|
||||
theme_importer = { path = "crates/theme_importer" }
|
||||
theme_selector = { path = "crates/theme_selector" }
|
||||
time_format = { path = "crates/time_format" }
|
||||
@@ -298,6 +317,7 @@ ui_macros = { path = "crates/ui_macros" }
|
||||
util = { path = "crates/util" }
|
||||
vcs_menu = { path = "crates/vcs_menu" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
welcome = { path = "crates/welcome" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
@@ -313,7 +333,7 @@ alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
arrayvec = { version = "0.7.4", features = ["serde"] }
|
||||
ashpd = "0.9.1"
|
||||
ashpd = { version = "0.10", default-features = false, features = ["async-std"]}
|
||||
async-compat = "0.2.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = "0.1"
|
||||
@@ -332,7 +352,7 @@ blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.18"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
@@ -368,14 +388,14 @@ indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "2"
|
||||
itertools = "0.13.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { version = "0.2.0" }
|
||||
jupyter-websocket-client = { version = "0.4.1" }
|
||||
jupyter-protocol = { version = "0.3.0" }
|
||||
jupyter-websocket-client = { version = "0.5.0" }
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nbformat = "0.6.0"
|
||||
nbformat = { version = "0.7.0" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
@@ -385,10 +405,10 @@ parking_lot = "0.12.1"
|
||||
pathdiff = "0.2"
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
profiling = "1"
|
||||
@@ -409,7 +429,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
|
||||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.21.0", default-features = false, features = [
|
||||
runtimelib = { version = "0.22.0", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-cursor"><path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4v-1"/><path d="M7 2h1a4 4 0 0 1 4 4v1"/></svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 20H16C14.9391 20 13.9217 19.6629 13.1716 19.0627C12.4214 18.4626 12 17.6487 12 16.8V7.2C12 6.35131 12.4214 5.53737 13.1716 4.93726C13.9217 4.33714 14.9391 4 16 4H17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 20H8C9.06087 20 10.0783 19.5786 10.8284 18.8284C11.5786 18.0783 12 17.0609 12 16V15" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 4H8C9.06087 4 10.0783 4.42143 10.8284 5.17157C11.5786 5.92172 12 6.93913 12 8V9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 715 B |
5
assets/icons/file_icons/diff.svg
Normal file
5
assets/icons/file_icons/diff.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.5 3L8.5 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 6.5H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 13H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 415 B |
@@ -34,6 +34,7 @@
|
||||
"dat": "storage",
|
||||
"db": "storage",
|
||||
"dbf": "storage",
|
||||
"diff": "diff",
|
||||
"dll": "storage",
|
||||
"doc": "document",
|
||||
"docx": "document",
|
||||
@@ -112,6 +113,7 @@
|
||||
"mkv": "video",
|
||||
"ml": "ocaml",
|
||||
"mli": "ocaml",
|
||||
"mod": "go",
|
||||
"mov": "video",
|
||||
"mp3": "audio",
|
||||
"mp4": "video",
|
||||
@@ -127,6 +129,7 @@
|
||||
"ogg": "audio",
|
||||
"opus": "audio",
|
||||
"otf": "font",
|
||||
"pcss": "css",
|
||||
"pdb": "storage",
|
||||
"pdf": "document",
|
||||
"php": "php",
|
||||
@@ -173,6 +176,9 @@
|
||||
"tsx": "react",
|
||||
"ttf": "font",
|
||||
"txt": "document",
|
||||
"v": "v",
|
||||
"vsh": "v",
|
||||
"vv": "v",
|
||||
"vue": "vue",
|
||||
"wav": "audio",
|
||||
"webm": "video",
|
||||
@@ -181,6 +187,7 @@
|
||||
"wmv": "video",
|
||||
"woff": "font",
|
||||
"woff2": "font",
|
||||
"work": "go",
|
||||
"wv": "audio",
|
||||
"xls": "document",
|
||||
"xlsx": "document",
|
||||
@@ -235,6 +242,9 @@
|
||||
"default": {
|
||||
"icon": "icons/file_icons/file.svg"
|
||||
},
|
||||
"diff": {
|
||||
"icon": "icons/file_icons/diff.svg"
|
||||
},
|
||||
"docker": {
|
||||
"icon": "icons/file_icons/docker.svg"
|
||||
},
|
||||
@@ -379,6 +389,9 @@
|
||||
"typescript": {
|
||||
"icon": "icons/file_icons/typescript.svg"
|
||||
},
|
||||
"v": {
|
||||
"icon": "icons/file_icons/v.svg"
|
||||
},
|
||||
"vcs": {
|
||||
"icon": "icons/file_icons/git.svg"
|
||||
},
|
||||
|
||||
4
assets/icons/file_icons/v.svg
Normal file
4
assets/icons/file_icons/v.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M10.0469 12.8661L13.3884 3.31889C13.4386 3.1754 13.3167 3.03055 13.1667 3.05554L10.7292 3.46179C10.5875 3.48542 10.4693 3.58324 10.4197 3.71807L7.24789 12.3271C7.12763 12.6536 7.36919 13 7.71706 13H9.8581C9.94309 13 10.0188 12.9463 10.0469 12.8661Z" fill="black"/>
|
||||
<path d="M6.90625 12.7321L3.61161 3.31889C3.56139 3.1754 3.6833 3.03055 3.83326 3.05554L6.27076 3.46179C6.4125 3.48542 6.53067 3.58324 6.58034 3.71807L9.90084 12.7309C9.94895 12.8614 9.85232 13 9.71317 13H7.28379C7.11381 13 6.9624 12.8926 6.90625 12.7321Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 663 B |
1
assets/icons/globe.svg
Normal file
1
assets/icons/globe.svg
Normal file
@@ -0,0 +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-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
|
||||
|
After Width: | Height: | Size: 327 B |
@@ -405,7 +405,7 @@
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"f1": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-e": "pane::RevealInProjectPanel",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-?": "assistant::ToggleFocus",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
@@ -594,6 +594,7 @@
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
@@ -649,11 +650,16 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder",
|
||||
"bindings": {
|
||||
"ctrl": "file_finder::ToggleMenu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder && !menu_open",
|
||||
"bindings": {
|
||||
"ctrl-shift-p": "file_finder::SelectPrev",
|
||||
"ctrl": "file_finder::OpenMenu",
|
||||
"ctrl-j": "pane::SplitDown",
|
||||
"ctrl-k": "pane::SplitUp",
|
||||
"ctrl-h": "pane::SplitLeft",
|
||||
|
||||
@@ -49,8 +49,9 @@
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
"ctrl-k": "editor::KillRingCut",
|
||||
"ctrl-y": "editor::KillRingYank",
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"cmd-k cmd-q": "editor::Rewrap",
|
||||
"cmd-backspace": "editor::DeleteToBeginningOfLine",
|
||||
@@ -92,6 +93,8 @@
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"cmd-up": "editor::MoveToBeginning",
|
||||
"cmd-down": "editor::MoveToEnd",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"shift-up": "editor::SelectUp",
|
||||
"ctrl-shift-p": "editor::SelectUp",
|
||||
"shift-down": "editor::SelectDown",
|
||||
@@ -206,6 +209,18 @@
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2",
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant2::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
@@ -340,7 +355,7 @@
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"cmd-shift-\\": "editor::MoveToEnclosingBracket",
|
||||
"cmd-|": "editor::MoveToEnclosingBracket",
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"cmd-k cmd-l": "editor::ToggleFold",
|
||||
@@ -431,7 +446,7 @@
|
||||
"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",
|
||||
"cmd-shift-e": "pane::RevealInProjectPanel",
|
||||
"cmd-shift-b": "outline_panel::ToggleFocus",
|
||||
"cmd-?": "assistant::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
@@ -601,6 +616,7 @@
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
@@ -647,11 +663,16 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder",
|
||||
"bindings": {
|
||||
"cmd": "file_finder::ToggleMenu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder && !menu_open",
|
||||
"bindings": {
|
||||
"cmd-shift-p": "file_finder::SelectPrev",
|
||||
"cmd": "file_finder::OpenMenu",
|
||||
"cmd-j": "pane::SplitDown",
|
||||
"cmd-k": "pane::SplitUp",
|
||||
"cmd-h": "pane::SplitLeft",
|
||||
@@ -711,7 +732,11 @@
|
||||
"cmd-end": "terminal::ScrollToBottom",
|
||||
"shift-home": "terminal::ScrollToTop",
|
||||
"shift-end": "terminal::ScrollToBottom",
|
||||
"ctrl-shift-space": "terminal::ToggleViMode"
|
||||
"ctrl-shift-space": "terminal::ToggleViMode",
|
||||
"ctrl-k up": "pane::SplitUp",
|
||||
"ctrl-k down": "pane::SplitDown",
|
||||
"ctrl-k left": "pane::SplitLeft",
|
||||
"ctrl-k right": "pane::SplitRight"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -55,6 +55,10 @@
|
||||
"n": "vim::MoveToNextMatch",
|
||||
"shift-n": "vim::MoveToPrevMatch",
|
||||
"%": "vim::Matching",
|
||||
"] }": ["vim::UnmatchedForward", { "char": "}" } ],
|
||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" } ],
|
||||
"] )": ["vim::UnmatchedForward", { "char": ")" } ],
|
||||
"[ (": ["vim::UnmatchedBackward", { "char": "(" } ],
|
||||
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
|
||||
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
|
||||
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
|
||||
@@ -381,8 +385,9 @@
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::AngleBrackets",
|
||||
"g": "vim::Argument"
|
||||
"a": "vim::Argument",
|
||||
"i": "vim::IndentObj",
|
||||
"shift-i": ["vim::IndentObj", { "includeBelow": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -552,6 +557,12 @@
|
||||
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w >": ["vim::ResizePane", "Widen"],
|
||||
"ctrl-w <": ["vim::ResizePane", "Narrow"],
|
||||
"ctrl-w -": ["vim::ResizePane", "Shorten"],
|
||||
"ctrl-w +": ["vim::ResizePane", "Lengthen"],
|
||||
"ctrl-w _": "vim::MaximizePane",
|
||||
"ctrl-w =": "vim::ResetPaneSizes",
|
||||
"ctrl-w g t": "pane::ActivateNextItem",
|
||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||
@@ -578,7 +589,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
|
||||
@@ -300,6 +300,8 @@
|
||||
"scroll_beyond_last_line": "one_page",
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
// Whether to scroll when clicking near the edge of the visible text area.
|
||||
"autoscroll_on_clicks": false,
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
@@ -490,9 +492,6 @@
|
||||
"version": "2",
|
||||
// Whether the assistant is enabled.
|
||||
"enabled": true,
|
||||
// Whether to show inline hints showing the keybindings to use the inline assistant and the
|
||||
// assistant panel.
|
||||
"show_hints": 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'.
|
||||
@@ -560,6 +559,8 @@
|
||||
"close_position": "right",
|
||||
// Whether to show the file icon for a tab.
|
||||
"file_icons": false,
|
||||
// Whether to always show the close button on tabs.
|
||||
"always_show_close_button": false,
|
||||
// What to do after closing the current tab.
|
||||
//
|
||||
// 1. Activate the tab that was open previously (default)
|
||||
@@ -668,7 +669,7 @@
|
||||
},
|
||||
// Add files or globs of files that will be excluded by Zed entirely:
|
||||
// they will be skipped during FS scan(s), file tree and file search
|
||||
// will lack the corresponding file entries.
|
||||
// will lack the corresponding file entries. Overrides `file_scan_inclusions`.
|
||||
"file_scan_exclusions": [
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
@@ -679,6 +680,11 @@
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
],
|
||||
// Add files or globs of files that will be included by Zed, even when
|
||||
// ignored by git. This is useful for files that are not tracked by git,
|
||||
// but are still important to your project. Note that globs that are
|
||||
// overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`.
|
||||
"file_scan_inclusions": [".env*"],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
@@ -839,8 +845,12 @@
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
// Whether to display the terminal title in its toolbar.
|
||||
"title": true
|
||||
// Whether to display the terminal title in its toolbar's breadcrumbs.
|
||||
// Only shown if the terminal title is not empty.
|
||||
//
|
||||
// The shell running in the terminal needs to be configured to emit the title.
|
||||
// Example: `echo -e "\e]2;New Title\007";`
|
||||
"breadcrumbs": true
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
@@ -876,7 +886,7 @@
|
||||
//
|
||||
"file_types": {
|
||||
"Plain Text": ["txt"],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json"],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
|
||||
"Shell Script": [".env.*"]
|
||||
},
|
||||
/// By default use a recent system version of node, or install our own.
|
||||
|
||||
@@ -33,7 +33,7 @@ client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_servers.workspace = true
|
||||
context_server.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
@@ -50,6 +50,8 @@ indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
|
||||
@@ -5,7 +5,6 @@ pub mod assistant_settings;
|
||||
mod context;
|
||||
pub mod context_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod patch;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
@@ -15,16 +14,12 @@ pub mod slash_command_settings;
|
||||
mod slash_command_working_set;
|
||||
mod streaming_diff;
|
||||
mod terminal_inline_assistant;
|
||||
mod tool_working_set;
|
||||
mod tools;
|
||||
|
||||
use crate::slash_command::project_command::ProjectSlashCommandFeatureFlag;
|
||||
pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet};
|
||||
pub use crate::tool_working_set::{ToolId, ToolWorkingSet};
|
||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use assistant_tool::ToolRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub use context::*;
|
||||
@@ -33,12 +28,10 @@ use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::impl_actions;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
pub(crate) use inline_assistant::*;
|
||||
use language_model::{
|
||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||
};
|
||||
pub(crate) use model_selector::*;
|
||||
pub use patch::*;
|
||||
pub use prompts::PromptBuilder;
|
||||
use prompts::PromptLoadingParams;
|
||||
@@ -249,7 +242,7 @@ pub fn init(
|
||||
assistant_slash_command::init(cx);
|
||||
assistant_tool::init(cx);
|
||||
assistant_panel::init(cx);
|
||||
context_servers::init(cx);
|
||||
context_server::init(cx);
|
||||
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
||||
fs: fs.clone(),
|
||||
@@ -262,7 +255,6 @@ pub fn init(
|
||||
.map(Arc::new)
|
||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||
register_slash_commands(Some(prompt_builder.clone()), cx);
|
||||
register_tools(cx);
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
@@ -275,7 +267,7 @@ pub fn init(
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
IndexedDocsRegistry::init_global(cx);
|
||||
indexed_docs::init(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
@@ -350,8 +342,7 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
||||
slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
|
||||
slash_command_registry.register_command(now_command::NowSlashCommand, false);
|
||||
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, true);
|
||||
|
||||
if let Some(prompt_builder) = prompt_builder {
|
||||
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
|
||||
@@ -426,11 +417,6 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
fn register_tools(cx: &mut AppContext) {
|
||||
let tool_registry = ToolRegistry::global(cx);
|
||||
tool_registry.register_tool(tools::now_tool::NowTool);
|
||||
}
|
||||
|
||||
pub fn humanize_token_count(count: usize) -> String {
|
||||
match count {
|
||||
0..=999 => count.to_string(),
|
||||
|
||||
@@ -12,17 +12,17 @@ use crate::{
|
||||
},
|
||||
slash_command_picker,
|
||||
terminal_inline_assistant::TerminalInlineAssistant,
|
||||
Assist, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context, ContextEvent,
|
||||
ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory,
|
||||
DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles, InsertIntoEditor,
|
||||
InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
|
||||
MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, ParsedSlashCommand,
|
||||
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, RequestType,
|
||||
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
|
||||
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
|
||||
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
|
||||
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
|
||||
InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
|
||||
MessageMetadata, MessageStatus, NewContext, ParsedSlashCommand, PendingSlashCommandStatus,
|
||||
QuoteSelection, RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
|
||||
ToggleModelSelector,
|
||||
};
|
||||
use crate::{PatchId, ResolvedPatch, ToolWorkingSet};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{proto, zed_urls, Client, Status};
|
||||
use collections::{hash_map, BTreeSet, HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -50,11 +50,12 @@ use indexed_docs::IndexedDocsStore;
|
||||
use language::{
|
||||
language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset,
|
||||
};
|
||||
use language_model::{
|
||||
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelRegistry, Role,
|
||||
};
|
||||
use language_model::{LanguageModelImage, LanguageModelToolUse};
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::lsp_store::LocalLspAdapterDelegate;
|
||||
@@ -142,7 +143,7 @@ pub struct AssistantPanel {
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
subscriptions: Vec<Subscription>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
model_summary_editor: View<Editor>,
|
||||
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
@@ -415,7 +416,6 @@ impl AssistantPanel {
|
||||
ControlFlow::Break(())
|
||||
});
|
||||
|
||||
pane.set_can_split(false, cx);
|
||||
pane.set_can_navigate(true, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_| true);
|
||||
@@ -450,6 +450,7 @@ impl AssistantPanel {
|
||||
.gap(DynamicSpacing::Base02.rems(cx))
|
||||
.child(
|
||||
IconButton::new("new-chat", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| {
|
||||
cx.dispatch_action(NewContext.boxed_clone())
|
||||
@@ -664,7 +665,7 @@ impl AssistantPanel {
|
||||
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||
// the provider, we want to show a nudge to sign in.
|
||||
let show_zed_ai_notice = client_status.is_signed_out()
|
||||
&& active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID);
|
||||
&& active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
|
||||
self.show_zed_ai_notice = show_zed_ai_notice;
|
||||
cx.notify();
|
||||
@@ -1315,7 +1316,7 @@ impl AssistantPanel {
|
||||
|
||||
fn restart_context_servers(
|
||||
workspace: &mut Workspace,
|
||||
_action: &context_servers::Restart,
|
||||
_action: &context_server::Restart,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
@@ -1481,15 +1482,13 @@ struct ScrollPosition {
|
||||
|
||||
struct PatchViewState {
|
||||
crease_id: CreaseId,
|
||||
multibuffer_range: Range<Anchor>,
|
||||
height: u32,
|
||||
editor: Option<PatchEditorState>,
|
||||
update_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
struct PatchEditorState {
|
||||
editor: WeakView<ProposedChangesEditor>,
|
||||
opened_patch: ResolvedPatch,
|
||||
opened_patch: AssistantPatch,
|
||||
}
|
||||
|
||||
type MessageHeader = MessageMetadata;
|
||||
@@ -1519,8 +1518,8 @@ pub struct ContextEditor {
|
||||
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
|
||||
pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
patches: HashMap<PatchId, PatchViewState>,
|
||||
active_patch: Option<PatchId>,
|
||||
patches: HashMap<Range<language::Anchor>, PatchViewState>,
|
||||
active_patch: Option<Range<language::Anchor>>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
last_error: Option<AssistError>,
|
||||
show_accept_terms: bool,
|
||||
@@ -1575,7 +1574,7 @@ impl ContextEditor {
|
||||
];
|
||||
|
||||
let sections = context.read(cx).slash_command_output_sections().to_vec();
|
||||
let patch_ids = context.read(cx).patch_ids().collect::<Vec<_>>();
|
||||
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
|
||||
let slash_commands = context.read(cx).slash_commands.clone();
|
||||
let tools = context.read(cx).tools.clone();
|
||||
let mut this = Self {
|
||||
@@ -1606,9 +1605,7 @@ impl ContextEditor {
|
||||
this.update_message_headers(cx);
|
||||
this.update_image_blocks(cx);
|
||||
this.insert_slash_command_output_sections(sections, false, cx);
|
||||
for patch_id in patch_ids {
|
||||
this.patch_updated(patch_id, cx);
|
||||
}
|
||||
this.patches_updated(&Vec::new(), &patch_ranges, cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -1640,7 +1637,7 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if let Some(patch) = self.active_patch() {
|
||||
if let Some((_range, patch)) = self.active_patch() {
|
||||
if let Some(editor) = patch
|
||||
.editor
|
||||
.as_ref()
|
||||
@@ -1928,7 +1925,7 @@ impl ContextEditor {
|
||||
Content::ToolUse {
|
||||
range: tool_use.source_range.clone(),
|
||||
tool_use: LanguageModelToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone(),
|
||||
input: tool_use.input.clone(),
|
||||
},
|
||||
@@ -1960,8 +1957,9 @@ impl ContextEditor {
|
||||
);
|
||||
});
|
||||
}
|
||||
ContextEvent::PatchUpdated(patch_id) => self.patch_updated(*patch_id, cx),
|
||||
ContextEvent::PatchRemoved(patch_id) => self.patch_removed(*patch_id, cx),
|
||||
ContextEvent::PatchesUpdated { removed, updated } => {
|
||||
self.patches_updated(removed, updated, cx);
|
||||
}
|
||||
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
@@ -2223,124 +2221,125 @@ impl ContextEditor {
|
||||
});
|
||||
}
|
||||
|
||||
fn patch_updated(&mut self, patch_id: PatchId, cx: &mut ViewContext<ContextEditor>) {
|
||||
fn patches_updated(
|
||||
&mut self,
|
||||
removed: &Vec<Range<text::Anchor>>,
|
||||
updated: &Vec<Range<text::Anchor>>,
|
||||
cx: &mut ViewContext<ContextEditor>,
|
||||
) {
|
||||
let this = cx.view().downgrade();
|
||||
let mut editors_to_close = Vec::new();
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let multibuffer = &snapshot.buffer_snapshot;
|
||||
let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
|
||||
|
||||
let Some(patch) = self.context.read(cx).patch_for_id(patch_id, cx) else {
|
||||
return;
|
||||
};
|
||||
let mut removed_crease_ids = Vec::new();
|
||||
let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
|
||||
for range in removed {
|
||||
if let Some(state) = self.patches.remove(range) {
|
||||
let patch_start = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, range.start)
|
||||
.unwrap();
|
||||
let patch_end = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, range.end)
|
||||
.unwrap();
|
||||
|
||||
let path_count = patch.path_count();
|
||||
let patch_start = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, patch.range.start)
|
||||
.unwrap();
|
||||
let patch_end = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, patch.range.end)
|
||||
.unwrap();
|
||||
let render_block: RenderBlock = Arc::new({
|
||||
let this = this.clone();
|
||||
move |cx: &mut BlockContext<'_, '_>| {
|
||||
let max_width = cx.max_width;
|
||||
let gutter_width = cx.gutter_dimensions.full_width();
|
||||
let block_id = cx.block_id;
|
||||
let selected = cx.selected;
|
||||
this.update(&mut **cx, |this, cx| {
|
||||
this.render_patch_block(
|
||||
patch_id,
|
||||
max_width,
|
||||
gutter_width,
|
||||
block_id,
|
||||
selected,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Empty.into_any())
|
||||
editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
|
||||
ranges_to_unfold.push(patch_start..patch_end);
|
||||
removed_crease_ids.push(state.crease_id);
|
||||
}
|
||||
});
|
||||
|
||||
let mut height = path_count as u32 + 1;
|
||||
if patch.status == AssistantPatchStatus::Pending {
|
||||
height += 1;
|
||||
}
|
||||
editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
|
||||
editor.remove_creases(removed_crease_ids, cx);
|
||||
|
||||
let crease = Crease::block(
|
||||
patch_start..patch_end,
|
||||
height,
|
||||
BlockStyle::Flex,
|
||||
render_block.clone(),
|
||||
);
|
||||
for range in updated {
|
||||
let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let should_refold;
|
||||
if let Some(state) = self.patches.get_mut(&patch_id) {
|
||||
if state.multibuffer_range != (patch_start..patch_end) || state.height != height {
|
||||
editor.remove_creases([state.crease_id], cx);
|
||||
state.crease_id = editor.insert_creases([crease.clone()], cx)[0];
|
||||
state.height = height;
|
||||
state.multibuffer_range = patch_start..patch_end;
|
||||
should_refold = snapshot.intersects_fold(patch_start.to_offset(&multibuffer));
|
||||
} else {
|
||||
should_refold = false;
|
||||
}
|
||||
|
||||
if state.editor.is_some() {
|
||||
let resolved_patch = self.context.read(cx).resolve_patch(patch_id, cx);
|
||||
state.update_task = Some({
|
||||
let this = this.clone();
|
||||
cx.spawn(|_, cx| async move {
|
||||
if let Some(resolved_patch) = resolved_patch.await.log_err() {
|
||||
Self::update_patch_editor(
|
||||
this.clone(),
|
||||
patch_id,
|
||||
resolved_patch,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
let path_count = patch.path_count();
|
||||
let patch_start = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, patch.range.start)
|
||||
.unwrap();
|
||||
let patch_end = multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, patch.range.end)
|
||||
.unwrap();
|
||||
let render_block: RenderBlock = Arc::new({
|
||||
let this = this.clone();
|
||||
let patch_range = range.clone();
|
||||
move |cx: &mut BlockContext<'_, '_>| {
|
||||
let max_width = cx.max_width;
|
||||
let gutter_width = cx.gutter_dimensions.full_width();
|
||||
let block_id = cx.block_id;
|
||||
let selected = cx.selected;
|
||||
this.update(&mut **cx, |this, cx| {
|
||||
this.render_patch_block(
|
||||
patch_range.clone(),
|
||||
max_width,
|
||||
gutter_width,
|
||||
block_id,
|
||||
selected,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
|
||||
self.patches.insert(
|
||||
patch_id,
|
||||
PatchViewState {
|
||||
crease_id,
|
||||
multibuffer_range: patch_start..patch_end,
|
||||
height,
|
||||
editor: None,
|
||||
update_task: None,
|
||||
},
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Empty.into_any())
|
||||
}
|
||||
});
|
||||
|
||||
let height = path_count as u32 + 1;
|
||||
let crease = Crease::block(
|
||||
patch_start..patch_end,
|
||||
height,
|
||||
BlockStyle::Flex,
|
||||
render_block.clone(),
|
||||
);
|
||||
|
||||
should_refold = true;
|
||||
}
|
||||
let should_refold;
|
||||
if let Some(state) = self.patches.get_mut(&range) {
|
||||
if let Some(editor_state) = &state.editor {
|
||||
if editor_state.opened_patch != patch {
|
||||
state.update_task = Some({
|
||||
let this = this.clone();
|
||||
cx.spawn(|_, cx| async move {
|
||||
Self::update_patch_editor(this.clone(), patch, cx)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if should_refold {
|
||||
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
|
||||
editor.fold_creases(vec![crease], false, cx);
|
||||
should_refold =
|
||||
snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
|
||||
} else {
|
||||
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
|
||||
self.patches.insert(
|
||||
range.clone(),
|
||||
PatchViewState {
|
||||
crease_id,
|
||||
editor: None,
|
||||
update_task: None,
|
||||
},
|
||||
);
|
||||
|
||||
should_refold = true;
|
||||
}
|
||||
|
||||
if should_refold {
|
||||
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
|
||||
editor.fold_creases(vec![crease], false, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.update_active_patch(cx);
|
||||
}
|
||||
|
||||
fn patch_removed(&mut self, patch_id: PatchId, cx: &mut ViewContext<ContextEditor>) {
|
||||
let Some(state) = self.patches.remove(&patch_id) else {
|
||||
return;
|
||||
};
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.unfold_ranges(&[state.multibuffer_range], true, false, cx);
|
||||
editor.remove_creases([state.crease_id], cx);
|
||||
});
|
||||
|
||||
if let Some(editor) = state.editor.and_then(|state| state.editor.upgrade()) {
|
||||
for editor in editors_to_close {
|
||||
self.close_patch_editor(editor, cx);
|
||||
}
|
||||
|
||||
self.update_active_patch(cx);
|
||||
}
|
||||
|
||||
@@ -2421,8 +2420,9 @@ impl ContextEditor {
|
||||
cx.emit(event.clone());
|
||||
}
|
||||
|
||||
fn active_patch(&self) -> Option<&PatchViewState> {
|
||||
self.patches.get(self.active_patch.as_ref()?)
|
||||
fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
|
||||
let patch = self.active_patch.as_ref()?;
|
||||
Some((patch.clone(), self.patches.get(&patch)?))
|
||||
}
|
||||
|
||||
fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -2431,8 +2431,9 @@ impl ContextEditor {
|
||||
});
|
||||
let context = self.context.read(cx);
|
||||
|
||||
let new_patch_id = context.patch_containing(newest_cursor, cx);
|
||||
if new_patch_id == self.active_patch {
|
||||
let new_patch = context.patch_containing(newest_cursor, cx).cloned();
|
||||
|
||||
if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2446,14 +2447,16 @@ impl ContextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(patch_id) = new_patch_id {
|
||||
self.active_patch = Some(patch_id);
|
||||
if let Some(new_patch) = new_patch {
|
||||
self.active_patch = Some(new_patch.range.clone());
|
||||
|
||||
if let Some(patch_state) = self.patches.get_mut(&patch_id) {
|
||||
let editor = patch_state
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|state| state.editor.upgrade());
|
||||
if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
|
||||
let mut editor = None;
|
||||
if let Some(state) = &patch_state.editor {
|
||||
if let Some(opened_editor) = state.editor.upgrade() {
|
||||
editor = Some(opened_editor);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(editor) = editor {
|
||||
self.workspace
|
||||
@@ -2462,13 +2465,8 @@ impl ContextEditor {
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
let resolved_patch = self.context.read(cx).resolve_patch(patch_id, cx);
|
||||
patch_state.update_task = Some(cx.spawn(move |this, cx| async move {
|
||||
if let Some(resolved_patch) = resolved_patch.await.log_err() {
|
||||
Self::open_patch_editor(this, patch_id, resolved_patch, cx)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
Self::open_patch_editor(this, new_patch, cx).await.log_err();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -2497,25 +2495,35 @@ impl ContextEditor {
|
||||
|
||||
async fn open_patch_editor(
|
||||
this: WeakView<Self>,
|
||||
patch_id: PatchId,
|
||||
patch: ResolvedPatch,
|
||||
patch: AssistantPatch,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let project = this.update(&mut cx, |this, _| this.project.clone())?;
|
||||
let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = ProposedChangesEditor::new(
|
||||
SharedString::from(""),
|
||||
Vec::<ProposedChangeLocation<usize>>::new(),
|
||||
let editor = ProposedChangesEditor::new(
|
||||
patch.title.clone(),
|
||||
resolved_patch
|
||||
.edit_groups
|
||||
.iter()
|
||||
.map(|(buffer, groups)| ProposedChangeLocation {
|
||||
buffer: buffer.clone(),
|
||||
ranges: groups
|
||||
.iter()
|
||||
.map(|group| group.context_range.clone())
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
Some(project.clone()),
|
||||
cx,
|
||||
);
|
||||
patch.apply(&mut editor, None, cx);
|
||||
resolved_patch.apply(&editor, cx);
|
||||
editor
|
||||
})?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(patch_state) = this.patches.get_mut(&patch_id) {
|
||||
if let Some(patch_state) = this.patches.get_mut(&patch.range) {
|
||||
patch_state.editor = Some(PatchEditorState {
|
||||
editor: editor.downgrade(),
|
||||
opened_patch: patch,
|
||||
@@ -2533,18 +2541,34 @@ impl ContextEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_patch_editor(
|
||||
async fn update_patch_editor(
|
||||
this: WeakView<Self>,
|
||||
patch_id: PatchId,
|
||||
patch: ResolvedPatch,
|
||||
patch: AssistantPatch,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let project = this.update(&mut cx, |this, _| this.project.clone())?;
|
||||
let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let patch_state = this.patches.get_mut(&patch_id)?;
|
||||
let patch_state = this.patches.get_mut(&patch.range)?;
|
||||
|
||||
let locations = resolved_patch
|
||||
.edit_groups
|
||||
.iter()
|
||||
.map(|(buffer, groups)| ProposedChangeLocation {
|
||||
buffer: buffer.clone(),
|
||||
ranges: groups
|
||||
.iter()
|
||||
.map(|group| group.context_range.clone())
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(state) = &mut patch_state.editor {
|
||||
if let Some(editor) = state.editor.upgrade() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
patch.apply(editor, Some(&state.opened_patch), cx);
|
||||
editor.set_title(patch.title.clone(), cx);
|
||||
editor.reset_locations(locations, cx);
|
||||
resolved_patch.apply(editor, cx);
|
||||
});
|
||||
|
||||
state.opened_patch = patch;
|
||||
@@ -2553,6 +2577,7 @@ impl ContextEditor {
|
||||
}
|
||||
}
|
||||
patch_state.update_task.take();
|
||||
|
||||
Some(())
|
||||
})?;
|
||||
Ok(())
|
||||
@@ -3427,16 +3452,23 @@ impl ContextEditor {
|
||||
|
||||
fn render_patch_block(
|
||||
&mut self,
|
||||
patch_id: PatchId,
|
||||
range: Range<text::Anchor>,
|
||||
max_width: Pixels,
|
||||
gutter_width: Pixels,
|
||||
id: BlockId,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
|
||||
let excerpt_id = *excerpt_id;
|
||||
let anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)
|
||||
.unwrap();
|
||||
|
||||
let theme = cx.theme().clone();
|
||||
let patch = self.context.read(cx).patch_for_id(patch_id, cx)?;
|
||||
let anchor = self.patches.get(&patch_id)?.multibuffer_range.start;
|
||||
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
|
||||
let paths = patch
|
||||
.paths()
|
||||
.map(|p| SharedString::from(p.to_string()))
|
||||
@@ -4426,13 +4458,13 @@ pub struct ContextEditorToolbarItem {
|
||||
fs: Arc<dyn Fs>,
|
||||
active_context_editor: Option<WeakView<ContextEditor>>,
|
||||
model_summary_editor: View<Editor>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ContextEditorToolbarItem {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
model_summary_editor: View<Editor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -4528,8 +4560,17 @@ impl Render for ContextEditorToolbarItem {
|
||||
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
|
||||
// })
|
||||
.child(
|
||||
ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
|
||||
@@ -5,13 +5,12 @@ use anthropic::Model as AnthropicModel;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Pixels};
|
||||
use language_model::provider::open_ai;
|
||||
use language_model::settings::{
|
||||
AnthropicSettingsContent, AnthropicSettingsContentV1, OllamaSettingsContent,
|
||||
OpenAiSettingsContent, OpenAiSettingsContentV1, VersionedAnthropicSettingsContent,
|
||||
VersionedOpenAiSettingsContent,
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use language_models::{
|
||||
provider::open_ai, AllLanguageModelSettings, AnthropicSettingsContent,
|
||||
AnthropicSettingsContentV1, OllamaSettingsContent, OpenAiSettingsContent,
|
||||
OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, VersionedOpenAiSettingsContent,
|
||||
};
|
||||
use language_model::{settings::AllLanguageModelSettings, CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -60,7 +59,6 @@ pub struct AssistantSettings {
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
pub show_hints: bool,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
@@ -203,7 +201,6 @@ impl AssistantSettingsContent {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
show_hints: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
@@ -244,7 +241,6 @@ impl AssistantSettingsContent {
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
@@ -357,7 +353,6 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
@@ -375,11 +370,6 @@ pub struct AssistantSettingsContentV2 {
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show inline hints that show keybindings for inline assistant
|
||||
/// and assistant panel.
|
||||
///
|
||||
/// Default: true
|
||||
show_hints: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
@@ -514,7 +504,6 @@ impl Settings for AssistantSettings {
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.show_hints, value.show_hints);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
@@ -585,7 +574,6 @@ mod tests {
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
#[cfg(test)]
|
||||
mod context_tests;
|
||||
|
||||
use crate::patch::{PatchId, PatchStore};
|
||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||
use crate::{
|
||||
prompts::PromptBuilder,
|
||||
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
|
||||
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
|
||||
};
|
||||
use crate::{PatchStoreEvent, ResolvedPatch, ToolWorkingSet};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{self, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt};
|
||||
use feature_flags::{FeatureFlagAppExt, ToolUseFeatureFlag};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{future::Shared, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
@@ -26,12 +25,14 @@ use gpui::{
|
||||
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
logging::report_assistant_event,
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
|
||||
StopReason,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent, Role, StopReason,
|
||||
};
|
||||
use language_models::{
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
report_assistant_event,
|
||||
};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use paths::contexts_dir;
|
||||
@@ -368,8 +369,10 @@ pub enum ContextEvent {
|
||||
MessagesEdited,
|
||||
SummaryChanged,
|
||||
StreamedCompletion,
|
||||
PatchUpdated(PatchId),
|
||||
PatchRemoved(PatchId),
|
||||
PatchesUpdated {
|
||||
removed: Vec<Range<language::Anchor>>,
|
||||
updated: Vec<Range<language::Anchor>>,
|
||||
},
|
||||
InvokedSlashCommandChanged {
|
||||
command_id: InvokedSlashCommandId,
|
||||
},
|
||||
@@ -382,7 +385,7 @@ pub enum ContextEvent {
|
||||
},
|
||||
UsePendingTools,
|
||||
ToolFinished {
|
||||
tool_use_id: Arc<str>,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output_range: Range<language::Anchor>,
|
||||
},
|
||||
Operation(ContextOperation),
|
||||
@@ -476,7 +479,7 @@ pub enum Content {
|
||||
},
|
||||
ToolResult {
|
||||
range: Range<language::Anchor>,
|
||||
tool_use_id: Arc<str>,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -543,7 +546,7 @@ pub struct Context {
|
||||
pub(crate) slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
pub(crate) tools: Arc<ToolWorkingSet>,
|
||||
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||
pending_tool_uses_by_id: HashMap<Arc<str>, PendingToolUse>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
message_anchors: Vec<MessageAnchor>,
|
||||
contents: Vec<Content>,
|
||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
@@ -559,10 +562,9 @@ pub struct Context {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
patches: Vec<(Range<text::Anchor>, PatchId)>,
|
||||
patch_store: Model<PatchStore>,
|
||||
patches: Vec<AssistantPatch>,
|
||||
xml_tags: Vec<XmlTag>,
|
||||
project: Model<Project>,
|
||||
project: Option<Model<Project>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
@@ -576,9 +578,9 @@ impl ContextAnnotation for ParsedSlashCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextAnnotation for (Range<language::Anchor>, PatchId) {
|
||||
impl ContextAnnotation for AssistantPatch {
|
||||
fn range(&self) -> &Range<language::Anchor> {
|
||||
&self.0
|
||||
&self.range
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,7 +595,7 @@ impl EventEmitter<ContextEvent> for Context {}
|
||||
impl Context {
|
||||
pub fn local(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project: Model<Project>,
|
||||
project: Option<Model<Project>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
@@ -623,7 +625,7 @@ impl Context {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
project: Model<Project>,
|
||||
project: Option<Model<Project>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
@@ -639,14 +641,6 @@ impl Context {
|
||||
});
|
||||
let edits_since_last_slash_command_parse =
|
||||
buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let patch_store = cx.new_model(|_| PatchStore::new(project.clone()));
|
||||
cx.subscribe(&patch_store, |_, _, event, cx| {
|
||||
cx.emit(match event {
|
||||
PatchStoreEvent::PatchUpdated(id) => ContextEvent::PatchUpdated(*id),
|
||||
PatchStoreEvent::PatchRemoved(id) => ContextEvent::PatchRemoved(*id),
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
let mut this = Self {
|
||||
id,
|
||||
timestamp: clock::Lamport::new(replica_id),
|
||||
@@ -671,7 +665,6 @@ impl Context {
|
||||
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
|
||||
pending_save: Task::ready(Ok(())),
|
||||
path: None,
|
||||
patch_store,
|
||||
buffer,
|
||||
telemetry,
|
||||
project,
|
||||
@@ -755,7 +748,7 @@ impl Context {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
project: Model<Project>,
|
||||
project: Option<Model<Project>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
@@ -1038,7 +1031,7 @@ impl Context {
|
||||
self.language_registry.clone()
|
||||
}
|
||||
|
||||
pub fn project(&self) -> Model<Project> {
|
||||
pub fn project(&self) -> Option<Model<Project>> {
|
||||
self.project.clone()
|
||||
}
|
||||
|
||||
@@ -1054,46 +1047,50 @@ impl Context {
|
||||
self.summary.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn patch_containing<'a>(
|
||||
&'a self,
|
||||
position: Point,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<PatchId> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self
|
||||
.patches
|
||||
.binary_search_by(|patch| {
|
||||
let patch_range = patch.0.to_point(&buffer);
|
||||
if position < patch_range.start {
|
||||
Ordering::Greater
|
||||
} else if position > patch_range.end {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some(self.patches[index].1)
|
||||
}
|
||||
|
||||
pub fn patch_ids(&self) -> impl Iterator<Item = PatchId> + '_ {
|
||||
self.patches.iter().map(|patch| patch.1)
|
||||
}
|
||||
|
||||
pub(crate) fn patch_for_id<'a>(
|
||||
&'a self,
|
||||
id: PatchId,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<&AssistantPatch> {
|
||||
self.patch_store.read(cx).get(id)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_patch(
|
||||
pub(crate) fn patch_containing(
|
||||
&self,
|
||||
id: PatchId,
|
||||
position: Point,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<ResolvedPatch>> {
|
||||
self.patch_store.read(cx).resolve_patch(id, cx)
|
||||
) -> Option<&AssistantPatch> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self.patches.binary_search_by(|patch| {
|
||||
let patch_range = patch.range.to_point(&buffer);
|
||||
if position < patch_range.start {
|
||||
Ordering::Greater
|
||||
} else if position > patch_range.end {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
if let Ok(ix) = index {
|
||||
Some(&self.patches[ix])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn patch_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
|
||||
self.patches.iter().map(|patch| patch.range.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn patch_for_range(
|
||||
&self,
|
||||
range: &Range<language::Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Option<&AssistantPatch> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self.patch_index_for_range(range, buffer).ok()?;
|
||||
Some(&self.patches[index])
|
||||
}
|
||||
|
||||
fn patch_index_for_range(
|
||||
&self,
|
||||
tagged_range: &Range<text::Anchor>,
|
||||
buffer: &text::BufferSnapshot,
|
||||
) -> Result<usize, usize> {
|
||||
self.patches
|
||||
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
|
||||
}
|
||||
|
||||
pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] {
|
||||
@@ -1129,7 +1126,7 @@ impl Context {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
pub fn get_tool_use_by_id(&self, id: &Arc<str>) -> Option<&PendingToolUse> {
|
||||
pub fn get_tool_use_by_id(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.get(id)
|
||||
}
|
||||
|
||||
@@ -1391,6 +1388,8 @@ impl Context {
|
||||
|
||||
let mut removed_parsed_slash_command_ranges = Vec::new();
|
||||
let mut updated_parsed_slash_commands = Vec::new();
|
||||
let mut removed_patches = Vec::new();
|
||||
let mut updated_patches = Vec::new();
|
||||
while let Some(mut row_range) = row_ranges.next() {
|
||||
while let Some(next_row_range) = row_ranges.peek() {
|
||||
if row_range.end >= next_row_range.start {
|
||||
@@ -1415,7 +1414,13 @@ impl Context {
|
||||
cx,
|
||||
);
|
||||
self.invalidate_pending_slash_commands(&buffer, cx);
|
||||
self.reparse_patches_in_range(start..end, &buffer, cx);
|
||||
self.reparse_patches_in_range(
|
||||
start..end,
|
||||
&buffer,
|
||||
&mut updated_patches,
|
||||
&mut removed_patches,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
if !updated_parsed_slash_commands.is_empty()
|
||||
@@ -1426,6 +1431,13 @@ impl Context {
|
||||
updated: updated_parsed_slash_commands,
|
||||
});
|
||||
}
|
||||
|
||||
if !updated_patches.is_empty() || !removed_patches.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated {
|
||||
removed: removed_patches,
|
||||
updated: updated_patches,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn reparse_slash_commands_in_range(
|
||||
@@ -1520,6 +1532,8 @@ impl Context {
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
buffer: &BufferSnapshot,
|
||||
updated: &mut Vec<Range<text::Anchor>>,
|
||||
removed: &mut Vec<Range<text::Anchor>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// Rebuild the XML tags in the edited range.
|
||||
@@ -1535,13 +1549,13 @@ impl Context {
|
||||
|
||||
// Reparse all tags after the last unchanged patch before the change.
|
||||
let mut tags_start_ix = 0;
|
||||
if let Some((preceding_unchanged_patch_range, _)) =
|
||||
if let Some(preceding_unchanged_patch) =
|
||||
self.patches[..intersecting_patches_range.start].last()
|
||||
{
|
||||
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
||||
tag.range
|
||||
.start
|
||||
.cmp(&preceding_unchanged_patch_range.end, buffer)
|
||||
.cmp(&preceding_unchanged_patch.range.end, buffer)
|
||||
.then(Ordering::Less)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
@@ -1550,28 +1564,13 @@ impl Context {
|
||||
|
||||
// Rebuild the patches in the range.
|
||||
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
|
||||
self.patch_store.update(cx, |patch_store, cx| {
|
||||
let mut removed_entries = self.patches[intersecting_patches_range.clone()]
|
||||
.iter()
|
||||
.map(|(range, id)| (range.start, *id))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let added_entries = new_patches.into_iter().map(|patch| {
|
||||
let id;
|
||||
let range = patch.range.clone();
|
||||
if let Some(existing_id) = removed_entries.remove(&range.start) {
|
||||
id = existing_id;
|
||||
patch_store.update(id, patch, cx).ok();
|
||||
} else {
|
||||
id = patch_store.insert(patch.clone(), cx);
|
||||
};
|
||||
(range, id)
|
||||
});
|
||||
self.patches
|
||||
.splice(intersecting_patches_range, added_entries);
|
||||
for id in removed_entries.into_values() {
|
||||
patch_store.remove(id, cx);
|
||||
}
|
||||
});
|
||||
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
|
||||
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
|
||||
removed.extend(
|
||||
removed_patches
|
||||
.map(|patch| patch.range)
|
||||
.filter(|range| !updated.contains(&range)),
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_xml_tags_in_range(
|
||||
@@ -1649,7 +1648,7 @@ impl Context {
|
||||
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
|
||||
patch_tag_depth += 1;
|
||||
let patch_start = tag.range.start;
|
||||
let mut edits = Vec::<AssistantEdit>::new();
|
||||
let mut edits = Vec::<Result<AssistantEdit>>::new();
|
||||
let mut patch = AssistantPatch {
|
||||
range: patch_start..patch_start,
|
||||
title: String::new().into(),
|
||||
@@ -1661,7 +1660,7 @@ impl Context {
|
||||
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
|
||||
patch_tag_depth -= 1;
|
||||
if patch_tag_depth == 0 {
|
||||
patch.range.end = tag.range.end.bias_right(buffer);
|
||||
patch.range.end = tag.range.end;
|
||||
|
||||
// Include the line immediately after this <patch> tag if it's empty.
|
||||
let patch_end_offset = patch.range.end.to_offset(buffer);
|
||||
@@ -1678,7 +1677,13 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
edits.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
if let (Ok(a), Ok(b)) = (a, b) {
|
||||
a.path.cmp(&b.path)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
patch.edits = edits.into();
|
||||
patch.status = AssistantPatchStatus::Ready;
|
||||
new_patches.push(patch);
|
||||
@@ -1708,17 +1713,13 @@ impl Context {
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
||||
if let Some(edit) = AssistantEdit::new(
|
||||
edits.push(AssistantEdit::new(
|
||||
path,
|
||||
operation,
|
||||
old_text,
|
||||
new_text,
|
||||
description,
|
||||
)
|
||||
.log_err()
|
||||
{
|
||||
edits.push(edit);
|
||||
}
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2152,7 +2153,7 @@ impl Context {
|
||||
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
tool_use_id: Arc<str>,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output: Task<Result<String>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -2339,11 +2340,10 @@ impl Context {
|
||||
let source_range = buffer.anchor_after(start_ix)
|
||||
..buffer.anchor_after(end_ix);
|
||||
|
||||
let tool_use_id: Arc<str> = tool_use.id.into();
|
||||
this.pending_tool_uses_by_id.insert(
|
||||
tool_use_id.clone(),
|
||||
tool_use.id.clone(),
|
||||
PendingToolUse {
|
||||
id: tool_use_id,
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
status: PendingToolUseStatus::Idle,
|
||||
@@ -2602,8 +2602,14 @@ impl Context {
|
||||
}
|
||||
|
||||
let buffer = self.buffer.read(cx).text_snapshot();
|
||||
let mut updated = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
for range in ranges {
|
||||
self.reparse_patches_in_range(range, &buffer, cx);
|
||||
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||
}
|
||||
|
||||
if !updated.is_empty() || !removed.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3194,19 +3200,9 @@ pub enum PendingSlashCommandStatus {
|
||||
Error(String),
|
||||
}
|
||||
|
||||
pub(crate) struct ToolUseFeatureFlag;
|
||||
|
||||
impl FeatureFlag for ToolUseFeatureFlag {
|
||||
const NAME: &'static str = "assistant-tool-use";
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PendingToolUse {
|
||||
pub id: Arc<str>,
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: String,
|
||||
pub input: serde_json::Value,
|
||||
pub status: PendingToolUseStatus,
|
||||
|
||||
@@ -5,12 +5,12 @@ use crate::{
|
||||
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
|
||||
MessageStatus, PromptBuilder,
|
||||
};
|
||||
use crate::{PatchId, ToolWorkingSet};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
|
||||
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
|
||||
};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{
|
||||
@@ -26,7 +26,6 @@ use project::Project;
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::mem;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
env,
|
||||
@@ -46,7 +45,23 @@ use workspace::Workspace;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
let context = init_test(cx);
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
@@ -171,7 +186,24 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_message_splitting(cx: &mut AppContext) {
|
||||
let context = init_test(cx);
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
LanguageModelRegistry::test(cx);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
@@ -259,7 +291,23 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
let context = init_test(cx);
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
@@ -338,7 +386,13 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.update(Project::init_settings);
|
||||
cx.update(assistant_panel::init);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
@@ -352,11 +406,24 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let context = cx.update(|cx| init_test_with_fs(fs, cx));
|
||||
|
||||
let slash_command_registry = cx.update(SlashCommandRegistry::default_global);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, false);
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
#[derive(Default)]
|
||||
struct ContextRanges {
|
||||
parsed_commands: HashSet<Range<language::Anchor>>,
|
||||
@@ -613,7 +680,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
cx.update(prompt_library::init);
|
||||
let mut settings_store = cx.update(SettingsStore::test);
|
||||
cx.update(|cx| {
|
||||
@@ -639,7 +706,7 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
project.clone(),
|
||||
Some(project),
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
@@ -648,17 +715,6 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
)
|
||||
});
|
||||
|
||||
let events = Rc::new(RefCell::new(Vec::new()));
|
||||
context.update(cx, |_, cx| {
|
||||
let events = events.clone();
|
||||
cx.subscribe(&context, move |_, _, event, _cx| match event {
|
||||
ContextEvent::PatchUpdated(id) => events.borrow_mut().push(("updated", *id)),
|
||||
ContextEvent::PatchRemoved(id) => events.borrow_mut().push(("removed", *id)),
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
// Insert an assistant message to simulate a response.
|
||||
let assistant_message_id = context.update(cx, |context, cx| {
|
||||
let user_message_id = context.messages(cx).next().unwrap().id;
|
||||
@@ -688,9 +744,8 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(mem::take(&mut *events.borrow_mut()), &[]);
|
||||
|
||||
// Partial patch tag is added
|
||||
// Partial edit step tag is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
@@ -726,7 +781,7 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
<edit>»",
|
||||
cx,
|
||||
);
|
||||
let patch_ids = expect_patches(
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
@@ -738,12 +793,8 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
&[&[]],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("updated", patch_ids[0])]
|
||||
);
|
||||
|
||||
// Add one edit to the patch
|
||||
// The full patch is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
@@ -760,59 +811,7 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>»",
|
||||
cx,
|
||||
);
|
||||
let patch_ids = expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn one</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>»",
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn one".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: Some("add a `two` function".into()),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("updated", patch_ids[0])]
|
||||
);
|
||||
|
||||
// The full patch is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn one</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>«
|
||||
</edit>
|
||||
</patch>
|
||||
|
||||
also,»",
|
||||
@@ -848,10 +847,6 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("updated", patch_ids[0])]
|
||||
);
|
||||
|
||||
// The step is manually edited.
|
||||
edit(
|
||||
@@ -906,10 +901,6 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("updated", patch_ids[0])]
|
||||
);
|
||||
|
||||
// When setting the message role to User, the steps are cleared.
|
||||
context.update(cx, |context, cx| {
|
||||
@@ -939,10 +930,6 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events.borrow_mut()),
|
||||
&[("removed", patch_ids[0])]
|
||||
);
|
||||
|
||||
// When setting the message role back to Assistant, the steps are reparsed.
|
||||
context.update(cx, |context, cx| {
|
||||
@@ -989,7 +976,7 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
project,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -1040,28 +1027,26 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
expected_marked_text: &str,
|
||||
expected_suggestions: &[&[AssistantEdit]],
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<PatchId> {
|
||||
) {
|
||||
let expected_marked_text = expected_marked_text.unindent();
|
||||
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
|
||||
|
||||
let (buffer_text, ranges, patch_ids, patches) = context.update(cx, |context, cx| {
|
||||
let patch_store = context.patch_store.read(cx);
|
||||
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
|
||||
context.buffer.read_with(cx, |buffer, _| {
|
||||
let ranges = context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|entry| entry.0.to_offset(buffer))
|
||||
.map(|entry| entry.range.to_offset(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
let patch_ids = context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|(_, id)| *id)
|
||||
.collect::<Vec<_>>();
|
||||
let patches = patch_ids
|
||||
.iter()
|
||||
.map(|id| patch_store.get(*id).unwrap().clone())
|
||||
.collect::<Vec<_>>();
|
||||
(buffer.text(), ranges, patch_ids, patches)
|
||||
(
|
||||
buffer.text(),
|
||||
ranges,
|
||||
context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|step| step.edits.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1075,24 +1060,41 @@ async fn test_patch_parsing(cx: &mut TestAppContext) {
|
||||
.iter()
|
||||
.map(|patch| {
|
||||
patch
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| AssistantEdit {
|
||||
path: edit.path.clone(),
|
||||
kind: edit.kind.clone(),
|
||||
.map(|edit| {
|
||||
let edit = edit.as_ref().unwrap();
|
||||
AssistantEdit {
|
||||
path: edit.path.clone(),
|
||||
kind: edit.kind.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
expected_suggestions
|
||||
);
|
||||
patch_ids
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_serialization(cx: &mut TestAppContext) {
|
||||
let context = cx.update(init_test);
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.update(assistant_panel::init);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
|
||||
let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
|
||||
let message_1 = context.update(cx, |context, cx| {
|
||||
@@ -1130,11 +1132,11 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
Context::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
context.read(cx).language_registry.clone(),
|
||||
context.read(cx).prompt_builder.clone(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
context.read(cx).project.clone(),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -1170,9 +1172,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.update(assistant_panel::init);
|
||||
cx.update(Project::init_settings);
|
||||
|
||||
cx.update(assistant_panel::init);
|
||||
let slash_commands = cx.update(SlashCommandRegistry::default_global);
|
||||
slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
|
||||
slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
|
||||
@@ -1186,8 +1187,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let context_id = ContextId::new();
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
for i in 0..num_peers {
|
||||
let project =
|
||||
cx.update(|cx| Project::empty(FakeFs::new(cx.background_executor().clone()), cx));
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
@@ -1197,7 +1196,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
project,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -1444,7 +1443,23 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|
||||
#[gpui::test]
|
||||
fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
let context = init_test(cx);
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = context.read(cx).buffer.clone();
|
||||
|
||||
// Create a test cache configuration
|
||||
@@ -1588,32 +1603,6 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut AppContext) -> Model<Context> {
|
||||
init_test_with_fs(FakeFs::new(cx.background_executor().clone()), cx)
|
||||
}
|
||||
|
||||
fn init_test_with_fs(fs: Arc<FakeFs>, cx: &mut AppContext) -> Model<Context> {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let project = Project::empty(fs, cx);
|
||||
cx.new_model(|cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
project,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role, Range<usize>)> {
|
||||
context
|
||||
.read(cx)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use crate::slash_command::context_server_command;
|
||||
use crate::SlashCommandId;
|
||||
use crate::{
|
||||
prompts::PromptBuilder, slash_command_working_set::SlashCommandWorkingSet, Context,
|
||||
ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, SavedContextMetadata,
|
||||
};
|
||||
use crate::{tools, SlashCommandId, ToolId, ToolWorkingSet};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_tool::{ToolId, ToolWorkingSet};
|
||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||
use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
use context_servers::manager::ContextServerManager;
|
||||
use context_servers::ContextServerFactoryRegistry;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
@@ -363,7 +364,7 @@ impl ContextStore {
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(
|
||||
self.languages.clone(),
|
||||
self.project.clone(),
|
||||
Some(self.project.clone()),
|
||||
Some(self.telemetry.clone()),
|
||||
self.prompt_builder.clone(),
|
||||
self.slash_commands.clone(),
|
||||
@@ -406,7 +407,7 @@ impl ContextStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
project,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
@@ -468,7 +469,7 @@ impl ContextStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
project,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
@@ -548,7 +549,7 @@ impl ContextStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
tools,
|
||||
project,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
@@ -770,7 +771,7 @@ impl ContextStore {
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
mtime: metadata.mtime.timestamp_for_user().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -808,13 +809,13 @@ impl ContextStore {
|
||||
fn handle_context_server_event(
|
||||
&mut self,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
event: &context_servers::manager::Event,
|
||||
event: &context_server::manager::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let slash_command_working_set = self.slash_commands.clone();
|
||||
let tool_working_set = self.tools.clone();
|
||||
match event {
|
||||
context_servers::manager::Event::ServerStarted { server_id } => {
|
||||
context_server::manager::Event::ServerStarted { server_id } => {
|
||||
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
|
||||
let context_server_manager = context_server_manager.clone();
|
||||
cx.spawn({
|
||||
@@ -825,7 +826,7 @@ impl ContextStore {
|
||||
return;
|
||||
};
|
||||
|
||||
if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
|
||||
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
|
||||
if let Some(prompts) = protocol.list_prompts().await.log_err() {
|
||||
let slash_command_ids = prompts
|
||||
.into_iter()
|
||||
@@ -853,12 +854,12 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
|
||||
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
if let Some(tools) = protocol.list_tools().await.log_err() {
|
||||
let tool_ids = tools.tools.into_iter().map(|tool| {
|
||||
log::info!("registering context server tool: {:?}", tool.name);
|
||||
tool_working_set.insert(
|
||||
Arc::new(tools::context_server_tool::ContextServerTool::new(
|
||||
Arc::new(ContextServerTool::new(
|
||||
context_server_manager.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
@@ -880,7 +881,7 @@ impl ContextStore {
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
context_servers::manager::Event::ServerStopped { server_id } => {
|
||||
context_server::manager::Event::ServerStopped { server_id } => {
|
||||
if let Some(slash_command_ids) =
|
||||
self.context_server_slash_command_ids.remove(server_id)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder,
|
||||
AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, RequestType, StreamingDiff,
|
||||
CyclePreviousInlineAssist, LineDiff, LineOperation, RequestType, StreamingDiff,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{telemetry::Telemetry, ErrorExt};
|
||||
@@ -30,14 +30,16 @@ use gpui::{
|
||||
};
|
||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
|
||||
use language_model::{
|
||||
logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role,
|
||||
};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use language_models::report_assistant_event;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use rope::Rope;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
cmp,
|
||||
@@ -1499,8 +1501,17 @@ impl Render for PromptEditor {
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -1520,7 +1531,7 @@ impl Render for PromptEditor {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.with_info_text(
|
||||
.info_text(
|
||||
"Inline edits use context\n\
|
||||
from the currently selected\n\
|
||||
assistant panel tab.",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,608 +0,0 @@
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext, Context, TestAppContext};
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use ui::BorrowAppContext;
|
||||
use unindent::Unindent as _;
|
||||
use util::test::{generate_marked_text, marked_text_ranges};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_patch_store(cx: &mut TestAppContext) {
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(language::init);
|
||||
cx.update(Project::init_settings);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"src": {
|
||||
"lib.rs": "
|
||||
fn one() -> usize {
|
||||
1
|
||||
}
|
||||
fn two() -> usize {
|
||||
2
|
||||
}
|
||||
fn three() -> usize {
|
||||
3
|
||||
}
|
||||
".unindent(),
|
||||
"main.rs": "
|
||||
use crate::one;
|
||||
fn main() { one(); }
|
||||
".unindent(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||
project.update(cx, |project, _| {
|
||||
project.languages().add(Arc::new(rust_lang()));
|
||||
});
|
||||
let patch_store = cx.new_model(|_| PatchStore::new(project.clone()));
|
||||
let context_buffer = cx.new_model(|cx| Buffer::local("hello", cx));
|
||||
let context_buffer = context_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let range = context_buffer.anchor_before(0)..context_buffer.anchor_before(1);
|
||||
|
||||
let patch_id = patch_store.update(cx, |store, cx| {
|
||||
store.insert(
|
||||
AssistantPatch {
|
||||
range: range.clone(),
|
||||
title: "first patch".into(),
|
||||
edits: vec![AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::Update {
|
||||
old_text: "1".into(),
|
||||
new_text: "100".into(),
|
||||
description: None,
|
||||
},
|
||||
}]
|
||||
.into(),
|
||||
status: AssistantPatchStatus::Pending,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
let patch = patch_store
|
||||
.update(cx, |store, cx| store.resolve_patch(patch_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_apply_patch(
|
||||
&patch,
|
||||
cx,
|
||||
&[(
|
||||
Path::new("src/lib.rs").into(),
|
||||
"
|
||||
fn one() -> usize {
|
||||
100
|
||||
}
|
||||
fn two() -> usize {
|
||||
2
|
||||
}
|
||||
fn three() -> usize {
|
||||
3
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
)],
|
||||
);
|
||||
|
||||
patch_store.update(cx, |store, cx| {
|
||||
store
|
||||
.update(
|
||||
patch_id,
|
||||
AssistantPatch {
|
||||
range: range.clone(),
|
||||
title: "first patch".into(),
|
||||
edits: vec![
|
||||
AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::Update {
|
||||
old_text: "1".into(),
|
||||
new_text: "100".into(),
|
||||
description: None,
|
||||
},
|
||||
},
|
||||
AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::Update {
|
||||
old_text: "3".into(),
|
||||
new_text: "300".into(),
|
||||
description: None,
|
||||
},
|
||||
},
|
||||
]
|
||||
.into(),
|
||||
status: AssistantPatchStatus::Pending,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
let patch = patch_store
|
||||
.update(cx, |store, cx| store.resolve_patch(patch_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_apply_patch(
|
||||
&patch,
|
||||
cx,
|
||||
&[(
|
||||
Path::new("src/lib.rs").into(),
|
||||
"
|
||||
fn one() -> usize {
|
||||
100
|
||||
}
|
||||
fn two() -> usize {
|
||||
2
|
||||
}
|
||||
fn three() -> usize {
|
||||
300
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_location(cx: &mut AppContext) {
|
||||
assert_location_resolution(
|
||||
concat!(
|
||||
" Lorem\n",
|
||||
"« ipsum\n",
|
||||
" dolor sit amet»\n",
|
||||
" consecteur",
|
||||
),
|
||||
"ipsum\ndolor",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
«fn foo1(a: usize) -> usize {
|
||||
40
|
||||
}»
|
||||
|
||||
fn foo2(b: usize) -> usize {
|
||||
42
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
"fn foo1(b: usize) {\n40\n}",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
fn main() {
|
||||
« Foo
|
||||
.bar()
|
||||
.baz()
|
||||
.qux()»
|
||||
}
|
||||
|
||||
fn foo2(b: usize) -> usize {
|
||||
42
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
"Foo.bar.baz.qux()",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
class Something {
|
||||
one() { return 1; }
|
||||
« two() { return 2222; }
|
||||
three() { return 333; }
|
||||
four() { return 4444; }
|
||||
five() { return 5555; }
|
||||
six() { return 6666; }
|
||||
» seven() { return 7; }
|
||||
eight() { return 8; }
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
&"
|
||||
two() { return 2222; }
|
||||
four() { return 4444; }
|
||||
five() { return 5555; }
|
||||
six() { return 6666; }
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_resolve_edits(cx: &mut TestAppContext) {
|
||||
cx.update(init_test);
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
/// A person
|
||||
struct Person {
|
||||
name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
/// A dog
|
||||
struct Dog {
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
name: String,
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn name(&self) -> String {
|
||||
format!(\"{} {}\", self.first_name, self.last_name)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
/// A person
|
||||
struct Person {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
/// A dog
|
||||
struct Dog {
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn name(&self) -> String {
|
||||
format!(\"{} {}\", self.first_name, self.last_name)
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Ensure InsertBefore merges correctly with Update of the same text
|
||||
assert_edits(
|
||||
"
|
||||
fn foo() {
|
||||
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::InsertBefore {
|
||||
old_text: "
|
||||
fn foo() {"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn bar() {
|
||||
qux();
|
||||
}"
|
||||
.unindent(),
|
||||
description: Some("implement bar".into()),
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn foo() {
|
||||
|
||||
}"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn foo() {
|
||||
bar();
|
||||
}"
|
||||
.unindent(),
|
||||
description: Some("call bar in foo".into()),
|
||||
},
|
||||
AssistantEditKind::InsertAfter {
|
||||
old_text: "
|
||||
fn foo() {
|
||||
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn qux() {
|
||||
// todo
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: Some("implement qux".into()),
|
||||
},
|
||||
],
|
||||
"
|
||||
fn bar() {
|
||||
qux();
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar();
|
||||
}
|
||||
|
||||
fn qux() {
|
||||
// todo
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Correctly indent new text when replacing multiple adjacent indented blocks.
|
||||
assert_edits(
|
||||
"
|
||||
impl Numbers {
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
|
||||
fn two() {
|
||||
2
|
||||
}
|
||||
|
||||
fn three() {
|
||||
3
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn two() {
|
||||
2
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn three() {
|
||||
3
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
impl Numbers {
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
impl Person {
|
||||
fn set_name(&mut self, name: String) {
|
||||
self.name = name;
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
return self.name;
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "self.name = name;".unindent(),
|
||||
new_text: "self._name = name;".unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "return self.name;\n".unindent(),
|
||||
new_text: "return self._name;\n".unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
impl Person {
|
||||
fn set_name(&mut self, name: String) {
|
||||
self._name = name;
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
return self._name;
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_apply_patch(
|
||||
patch: &ResolvedPatch,
|
||||
cx: &mut TestAppContext,
|
||||
expected_output: &[(Arc<Path>, String)],
|
||||
) {
|
||||
let mut actual_output = Vec::new();
|
||||
for (buffer, edit_groups) in &patch.edit_groups {
|
||||
let branch = buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
cx.update(|cx| {
|
||||
ResolvedPatch::apply_buffer_edits(&Vec::new(), edit_groups, &branch, cx);
|
||||
actual_output.push((
|
||||
buffer.read(cx).file().unwrap().path().clone(),
|
||||
branch.read(cx).text(),
|
||||
));
|
||||
});
|
||||
}
|
||||
pretty_assertions::assert_eq!(actual_output, expected_output);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut AppContext) {
|
||||
let (text, _) = marked_text_ranges(text_with_expected_range, false);
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let range = AssistantEditKind::resolve_location(snapshot.as_rope(), query).to_offset(&snapshot);
|
||||
let text_with_actual_range = generate_marked_text(&text, &[range], false);
|
||||
pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
|
||||
}
|
||||
|
||||
async fn assert_edits(
|
||||
old_text: String,
|
||||
edits: Vec<AssistantEditKind>,
|
||||
new_text: String,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({"file.rs": old_text})).await;
|
||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||
project.update(cx, |project, _| {
|
||||
project.languages().add(Arc::new(rust_lang()));
|
||||
});
|
||||
let patch_store = cx.new_model(|_| PatchStore::new(project));
|
||||
let patch_range = language::Anchor::MIN..language::Anchor::MAX;
|
||||
let patch_id = patch_store.update(cx, |patch_store, cx| {
|
||||
patch_store.insert(
|
||||
AssistantPatch {
|
||||
range: patch_range.clone(),
|
||||
title: "test-patch".into(),
|
||||
edits: edits
|
||||
.into_iter()
|
||||
.map(|kind| AssistantEdit {
|
||||
path: "file.rs".into(),
|
||||
kind,
|
||||
})
|
||||
.collect(),
|
||||
status: AssistantPatchStatus::Ready,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let patch = patch_store
|
||||
.update(cx, |patch_store, cx| {
|
||||
patch_store.resolve_patch(patch_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (buffer, edit_groups) = patch.edit_groups.into_iter().next().unwrap();
|
||||
cx.update(|cx| ResolvedPatch::apply_buffer_edits(&Vec::new(), &edit_groups, &buffer, cx));
|
||||
let actual_new_text = buffer.read_with(cx, |buffer, _| buffer.text());
|
||||
pretty_assertions::assert_eq!(actual_new_text, new_text);
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(language::tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use assistant_slash_command::{
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use context_servers::{
|
||||
use context_server::{
|
||||
manager::{ContextServer, ContextServerManager},
|
||||
types::Prompt,
|
||||
};
|
||||
@@ -95,9 +95,9 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
|
||||
let completion_result = protocol
|
||||
.completion(
|
||||
context_servers::types::CompletionReference::Prompt(
|
||||
context_servers::types::PromptReference {
|
||||
r#type: context_servers::types::PromptReferenceType::Prompt,
|
||||
context_server::types::CompletionReference::Prompt(
|
||||
context_server::types::PromptReference {
|
||||
r#type: context_server::types::PromptReferenceType::Prompt,
|
||||
name: prompt_name,
|
||||
},
|
||||
),
|
||||
@@ -152,7 +152,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
if result
|
||||
.messages
|
||||
.iter()
|
||||
.any(|msg| !matches!(msg.role, context_servers::types::Role::User))
|
||||
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Prompt contains non-user roles, which is not supported"
|
||||
@@ -164,7 +164,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
.messages
|
||||
.into_iter()
|
||||
.filter_map(|msg| match msg.content {
|
||||
context_servers::types::MessageContent::Text { text } => Some(text),
|
||||
context_server::types::MessageContent::Text { text } => Some(text),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
|
||||
@@ -108,6 +108,10 @@ impl SlashCommand for FetchSlashCommand {
|
||||
"Insert fetched URL contents".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Globe
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
@@ -162,7 +166,7 @@ impl SlashCommand for FetchSlashCommand {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::AtSign,
|
||||
icon: IconName::Globe,
|
||||
label: format!("fetch {}", url).into(),
|
||||
metadata: None,
|
||||
}],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use crate::{
|
||||
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
|
||||
ModelSelector, RequestType, DEFAULT_CONTEXT_LINES,
|
||||
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, RequestType,
|
||||
DEFAULT_CONTEXT_LINES,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
@@ -17,10 +18,11 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use settings::Settings;
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use language_models::report_assistant_event;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use std::{
|
||||
cmp,
|
||||
sync::Arc,
|
||||
@@ -30,7 +32,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
@@ -612,8 +614,17 @@ impl Render for PromptEditor {
|
||||
.w_12()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
.child(LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -693,7 +704,7 @@ impl PromptEditor {
|
||||
cx,
|
||||
);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_placeholder_text("Add a prompt…", cx);
|
||||
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -726,6 +737,14 @@ impl PromptEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn placeholder_text(cx: &WindowContext) -> String {
|
||||
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
|
||||
.map(|keybinding| format!(" • {keybinding} for context"))
|
||||
.unwrap_or_default();
|
||||
|
||||
format!("Generate…{context_keybinding} • ↓↑ for history")
|
||||
}
|
||||
|
||||
fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor_subscriptions.clear();
|
||||
self.editor_subscriptions
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod context_server_tool;
|
||||
pub mod now_tool;
|
||||
39
crates/assistant2/Cargo.toml
Normal file
39
crates/assistant2/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_server.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
settings.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
46
crates/assistant2/src/assistant.rs
Normal file
46
crates/assistant2/src/assistant.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
mod assistant_panel;
|
||||
mod message_editor;
|
||||
mod thread;
|
||||
mod thread_store;
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use gpui::{actions, AppContext};
|
||||
|
||||
pub use crate::assistant_panel::AssistantPanel;
|
||||
|
||||
actions!(
|
||||
assistant2,
|
||||
[ToggleFocus, NewThread, ToggleModelSelector, Chat]
|
||||
);
|
||||
|
||||
const NAMESPACE: &str = "assistant2";
|
||||
|
||||
/// Initializes the `assistant2` crate.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
feature_gate_assistant2_actions(cx);
|
||||
}
|
||||
|
||||
fn feature_gate_assistant2_actions(cx: &mut AppContext) {
|
||||
const ASSISTANT1_NAMESPACE: &str = "assistant";
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
});
|
||||
|
||||
cx.observe_flag::<Assistant2FeatureFlag, _>(move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_namespace(NAMESPACE);
|
||||
filter.hide_namespace(ASSISTANT1_NAMESPACE);
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
filter.show_namespace(ASSISTANT1_NAMESPACE);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
512
crates/assistant2/src/assistant_panel.rs
Normal file
512
crates/assistant2/src/assistant_panel.rs
Normal file
@@ -0,0 +1,512 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::zed_urls;
|
||||
use gpui::{
|
||||
prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
|
||||
FocusableView, FontWeight, Model, Pixels, Subscription, Task, View, ViewContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, Role};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::message_editor::MessageEditor;
|
||||
use crate::thread::{Message, Thread, ThreadError, ThreadEvent};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{NewThread, ToggleFocus, ToggleModelSelector};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
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 struct AssistantPanel {
|
||||
workspace: WeakView<Workspace>,
|
||||
#[allow(unused)]
|
||||
thread_store: Model<ThreadStore>,
|
||||
thread: Model<Thread>,
|
||||
message_editor: View<MessageEditor>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
last_error: Option<ThreadError>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let tools = Arc::new(ToolWorkingSet::default());
|
||||
let thread_store = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
ThreadStore::new(project, tools.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn new(
|
||||
workspace: &Workspace,
|
||||
thread_store: Model<ThreadStore>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let thread = cx.new_model(|cx| Thread::new(tools.clone(), cx));
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&thread, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
Self {
|
||||
workspace: workspace.weak_handle(),
|
||||
thread_store,
|
||||
thread: thread.clone(),
|
||||
message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
|
||||
tools,
|
||||
last_error: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let tools = self.thread.read(cx).tools().clone();
|
||||
let thread = cx.new_model(|cx| Thread::new(tools, cx));
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&thread, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx));
|
||||
self.thread = thread;
|
||||
self._subscriptions = subscriptions;
|
||||
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
&mut self,
|
||||
_: Model<Thread>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::ShowError(error) => {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
ThreadEvent::StreamedCompletion => {}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_tool_output(
|
||||
tool_use.assistant_message_id,
|
||||
tool_use.id.clone(),
|
||||
task,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolFinished { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.message_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for AssistantPanel {}
|
||||
|
||||
impl Panel for AssistantPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"AssistantPanel2"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
||||
DockPosition::Right
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, _: DockPosition) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
px(640.)
|
||||
}
|
||||
|
||||
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn remote_id() -> Option<proto::PanelId> {
|
||||
Some(proto::PanelId::AssistantPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
Some(IconName::ZedAssistant)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
Some("Assistant Panel")
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
h_flex()
|
||||
.id("assistant-toolbar")
|
||||
.justify_between()
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.h(Tab::container_height(cx))
|
||||
.px(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(h_flex().child(Label::new("Thread Title Goes Here")))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
IconButton::new("new-thread", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"New Thread",
|
||||
&NewThread,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_event, _cx| {
|
||||
println!("New Thread");
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-history", IconName::HistoryRerun)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Open History", cx))
|
||||
.on_click(move |_event, _cx| {
|
||||
println!("Open History");
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("configure-assistant", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
|
||||
.on_click(move |_event, _cx| {
|
||||
println!("Configure Assistant");
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match (active_provider, active_model) {
|
||||
(Some(provider), Some(model)) => h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(
|
||||
model.icon().unwrap_or_else(|| provider.icon()),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message(&self, message: Message, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let (role_icon, role_name) = match message.role {
|
||||
Role::User => (IconName::Person, "You"),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
|
||||
Role::System => (IconName::Settings, "System"),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.p_1p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(role_icon).size(IconSize::Small))
|
||||
.child(Label::new(role_name).size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.child(v_flex().p_1p5().child(Label::new(message.text.clone())))
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.last_error.as_ref()?;
|
||||
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
|
||||
ThreadError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
}
|
||||
ThreadError::Message(error_message) => {
|
||||
self.render_error_message(error_message, cx)
|
||||
}
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
|
||||
cx.listener(|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_error_message(
|
||||
&self,
|
||||
error_message: &SharedString,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(
|
||||
Label::new("Error interacting with language model")
|
||||
.weight(FontWeight::MEDIUM),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_32()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let messages = self.thread.read(cx).messages().cloned().collect::<Vec<_>>();
|
||||
|
||||
v_flex()
|
||||
.key_context("AssistantPanel2")
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.on_action(cx.listener(|this, _: &NewThread, cx| {
|
||||
this.new_thread(cx);
|
||||
}))
|
||||
.child(self.render_toolbar(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.id("message-list")
|
||||
.gap_2()
|
||||
.size_full()
|
||||
.p_2()
|
||||
.overflow_y_scroll()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.children(
|
||||
messages
|
||||
.into_iter()
|
||||
.map(|message| self.render_message(message, cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(self.message_editor.clone()),
|
||||
)
|
||||
.children(self.render_last_error(cx))
|
||||
}
|
||||
}
|
||||
167
crates/assistant2/src/message_editor.rs
Normal file
167
crates/assistant2/src/message_editor.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding};
|
||||
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::Chat;
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
use_tools: bool,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_placeholder_text("Ask anything…", cx);
|
||||
|
||||
editor
|
||||
}),
|
||||
use_tools: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
self.send_to_model(RequestKind::Chat, cx);
|
||||
}
|
||||
|
||||
fn send_to_model(
|
||||
&mut self,
|
||||
request_kind: RequestKind,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<()> {
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
if provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||
{
|
||||
cx.notify();
|
||||
return None;
|
||||
}
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model = model_registry.active_model()?;
|
||||
|
||||
let user_message = self.editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
editor.clear(cx);
|
||||
text
|
||||
});
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(user_message);
|
||||
let mut request = thread.to_completion_request(request_kind, cx);
|
||||
|
||||
if self.use_tools {
|
||||
request.tools = thread
|
||||
.tools()
|
||||
.tools(cx)
|
||||
.into_iter()
|
||||
.map(|tool| LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool.input_schema(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
thread.stream_completion(request, model, cx)
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for MessageEditor {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Button::new("add-context", "Add Context")
|
||||
.style(ButtonStyle::Filled)
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start),
|
||||
)
|
||||
.child(CheckboxWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools"),
|
||||
self.use_tools.into(),
|
||||
cx.listener(|this, selection, _cx| {
|
||||
this.use_tools = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
};
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
|
||||
.child(Label::new("or"))
|
||||
.child(
|
||||
ButtonLike::new("chat")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Chat"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.on_click(move |_event, cx| {
|
||||
focus_handle.dispatch_action(&Chat, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
354
crates/assistant2/src/thread.rs
Normal file
354
crates/assistant2/src/thread.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use collections::HashMap;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
|
||||
StopReason,
|
||||
};
|
||||
use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::post_inc;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
Chat,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct MessageId(usize);
|
||||
|
||||
impl MessageId {
|
||||
fn post_inc(&mut self) -> Self {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// A message in a [`Thread`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub id: MessageId,
|
||||
pub role: Role,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// A thread of conversation with the LLM.
|
||||
pub struct Thread {
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tool_uses_by_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
tool_results_by_message: HashMap<MessageId, Vec<LanguageModelToolResult>>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
|
||||
Self {
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
tools,
|
||||
tool_uses_by_message: HashMap::default(),
|
||||
tool_results_by_message: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> impl Iterator<Item = &Message> {
|
||||
self.messages.iter()
|
||||
}
|
||||
|
||||
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
|
||||
&self.tools
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
pub fn insert_user_message(&mut self, text: impl Into<String>) {
|
||||
self.messages.push(Message {
|
||||
id: self.next_message_id.post_inc(),
|
||||
role: Role::User,
|
||||
text: text.into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
_request_kind: RequestKind,
|
||||
_cx: &AppContext,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
for message in &self.messages {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: message.role,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
if let Some(tool_results) = self.tool_results_by_message.get(&message.id) {
|
||||
for tool_result in tool_results {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::ToolResult(tool_result.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if !message.text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message.text.clone()));
|
||||
}
|
||||
|
||||
if let Some(tool_uses) = self.tool_uses_by_message.get(&message.id) {
|
||||
for tool_use in tool_uses {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::ToolUse(tool_use.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
request.messages.push(request_message);
|
||||
}
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
pub fn stream_completion(
|
||||
&mut self,
|
||||
request: LanguageModelRequest,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let pending_completion_id = post_inc(&mut self.completion_count);
|
||||
|
||||
let task = cx.spawn(|thread, mut cx| async move {
|
||||
let stream = model.stream_completion(request, &cx);
|
||||
let stream_completion = async {
|
||||
let mut events = stream.await?;
|
||||
let mut stop_reason = StopReason::EndTurn;
|
||||
|
||||
while let Some(event) = events.next().await {
|
||||
let event = event?;
|
||||
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
match event {
|
||||
LanguageModelCompletionEvent::StartMessage { .. } => {
|
||||
thread.messages.push(Message {
|
||||
id: thread.next_message_id.post_inc(),
|
||||
role: Role::Assistant,
|
||||
text: String::new(),
|
||||
});
|
||||
}
|
||||
LanguageModelCompletionEvent::Stop(reason) => {
|
||||
stop_reason = reason;
|
||||
}
|
||||
LanguageModelCompletionEvent::Text(chunk) => {
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
if last_message.role == Role::Assistant {
|
||||
last_message.text.push_str(&chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(tool_use) => {
|
||||
if let Some(last_assistant_message) = thread
|
||||
.messages
|
||||
.iter()
|
||||
.rfind(|message| message.role == Role::Assistant)
|
||||
{
|
||||
thread
|
||||
.tool_uses_by_message
|
||||
.entry(last_assistant_message.id)
|
||||
.or_default()
|
||||
.push(tool_use.clone());
|
||||
|
||||
thread.pending_tool_uses_by_id.insert(
|
||||
tool_use.id.clone(),
|
||||
PendingToolUse {
|
||||
assistant_message_id: last_assistant_message.id,
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
status: PendingToolUseStatus::Idle,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::StreamedCompletion);
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
|
||||
thread.update(&mut cx, |thread, _cx| {
|
||||
thread
|
||||
.pending_completions
|
||||
.retain(|completion| completion.id != pending_completion_id);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(stop_reason)
|
||||
};
|
||||
|
||||
let result = stream_completion.await;
|
||||
|
||||
thread
|
||||
.update(&mut cx, |_thread, cx| match result.as_ref() {
|
||||
Ok(stop_reason) => match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
cx.emit(ThreadEvent::UsePendingTools);
|
||||
}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
},
|
||||
Err(error) => {
|
||||
if error.is::<PaymentRequiredError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
|
||||
} else if error.is::<MaxMonthlySpendReachedError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
|
||||
} else {
|
||||
let error_message = error
|
||||
.chain()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
|
||||
SharedString::from(error_message.clone()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
self.pending_completions.push(PendingCompletion {
|
||||
id: pending_completion_id,
|
||||
_task: task,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output: Task<Result<String>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let insert_output_task = cx.spawn(|thread, mut cx| {
|
||||
let tool_use_id = tool_use_id.clone();
|
||||
async move {
|
||||
let output = output.await;
|
||||
thread
|
||||
.update(&mut cx, |thread, cx| {
|
||||
// The tool use was requested by an Assistant message,
|
||||
// so we want to attach the tool results to the next
|
||||
// user message.
|
||||
let next_user_message = MessageId(assistant_message_id.0 + 1);
|
||||
|
||||
let tool_results = thread
|
||||
.tool_results_by_message
|
||||
.entry(next_user_message)
|
||||
.or_default();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
tool_results.push(LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
content: output,
|
||||
is_error: false,
|
||||
});
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
|
||||
}
|
||||
Err(err) => {
|
||||
tool_results.push(LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
content: err.to_string(),
|
||||
is_error: true,
|
||||
});
|
||||
|
||||
if let Some(tool_use) =
|
||||
thread.pending_tool_uses_by_id.get_mut(&tool_use_id)
|
||||
{
|
||||
tool_use.status = PendingToolUseStatus::Error(err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||
tool_use.status = PendingToolUseStatus::Running {
|
||||
_task: insert_output_task.shared(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ThreadError {
|
||||
PaymentRequired,
|
||||
MaxMonthlySpendReached,
|
||||
Message(SharedString),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ThreadEvent {
|
||||
ShowError(ThreadError),
|
||||
StreamedCompletion,
|
||||
UsePendingTools,
|
||||
ToolFinished {
|
||||
#[allow(unused)]
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventEmitter<ThreadEvent> for Thread {}
|
||||
|
||||
struct PendingCompletion {
|
||||
id: usize,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PendingToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
/// The ID of the Assistant message in which the tool use was requested.
|
||||
pub assistant_message_id: MessageId,
|
||||
pub name: String,
|
||||
pub input: serde_json::Value,
|
||||
pub status: PendingToolUseStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PendingToolUseStatus {
|
||||
Idle,
|
||||
Running { _task: Shared<Task<()>> },
|
||||
Error(#[allow(unused)] String),
|
||||
}
|
||||
|
||||
impl PendingToolUseStatus {
|
||||
pub fn is_idle(&self) -> bool {
|
||||
matches!(self, PendingToolUseStatus::Idle)
|
||||
}
|
||||
}
|
||||
114
crates/assistant2/src/thread_store.rs
Normal file
114
crates/assistant2/src/thread_store.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolId, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use gpui::{prelude::*, AppContext, Model, ModelContext, Task};
|
||||
use project::Project;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct ThreadStore {
|
||||
#[allow(unused)]
|
||||
project: Model<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
}
|
||||
|
||||
impl ThreadStore {
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new_model(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
|
||||
let this = Self {
|
||||
project,
|
||||
tools,
|
||||
context_server_manager,
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
};
|
||||
this.register_context_server_handlers(cx);
|
||||
|
||||
this
|
||||
})?;
|
||||
|
||||
Ok(this)
|
||||
})
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
|
||||
cx.subscribe(
|
||||
&self.context_server_manager.clone(),
|
||||
Self::handle_context_server_event,
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_context_server_event(
|
||||
&mut self,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
event: &context_server::manager::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let tool_working_set = self.tools.clone();
|
||||
match event {
|
||||
context_server::manager::Event::ServerStarted { server_id } => {
|
||||
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
|
||||
let context_server_manager = context_server_manager.clone();
|
||||
cx.spawn({
|
||||
let server = server.clone();
|
||||
let server_id = server_id.clone();
|
||||
|this, mut cx| async move {
|
||||
let Some(protocol) = server.client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
if let Some(tools) = protocol.list_tools().await.log_err() {
|
||||
let tool_ids = tools
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
log::info!(
|
||||
"registering context server tool: {:?}",
|
||||
tool.name
|
||||
);
|
||||
tool_working_set.insert(Arc::new(
|
||||
ContextServerTool::new(
|
||||
context_server_manager.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.context_server_tool_ids.insert(server_id, tool_ids);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
context_server::manager::Event::ServerStopped { server_id } => {
|
||||
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
|
||||
tool_working_set.remove(&tool_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use workspace::{ui::IconName, Workspace};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
extension_slash_command::init(cx);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -3,17 +3,39 @@ use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use extension::{Extension, WorktreeDelegate};
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
SlashCommandRegistry, SlashCommandResult,
|
||||
};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let proxy = ExtensionHostProxy::default_global(cx);
|
||||
proxy.register_slash_command_proxy(SlashCommandRegistryProxy {
|
||||
slash_command_registry: SlashCommandRegistry::global(cx),
|
||||
});
|
||||
}
|
||||
|
||||
struct SlashCommandRegistryProxy {
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
}
|
||||
|
||||
impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
|
||||
fn register_slash_command(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
command: extension::SlashCommand,
|
||||
) {
|
||||
self.slash_command_registry
|
||||
.register_command(ExtensionSlashCommand::new(extension, command), false)
|
||||
}
|
||||
}
|
||||
|
||||
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
|
||||
struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod tool_registry;
|
||||
mod tool_working_set;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -6,7 +7,8 @@ use anyhow::Result;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use tool_registry::*;
|
||||
pub use crate::tool_registry::*;
|
||||
pub use crate::tool_working_set::*;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
ToolRegistry::default_global(cx);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use assistant_tool::{Tool, ToolRegistry};
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::AppContext;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{Tool, ToolRegistry};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub struct ToolId(usize);
|
||||
22
crates/assistant_tools/Cargo.toml
Normal file
22
crates/assistant_tools/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "assistant_tools"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_tools.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
gpui.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace.workspace = true
|
||||
13
crates/assistant_tools/src/assistant_tools.rs
Normal file
13
crates/assistant_tools/src/assistant_tools.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
mod now_tool;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use gpui::AppContext;
|
||||
|
||||
use crate::now_tool::NowTool;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(NowTool);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ impl Tool for NowTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Returns the current datetime in RFC 3339 format.".into()
|
||||
"Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
@@ -18,5 +18,5 @@ collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rodio = { version = "0.19.0", default-features = false, features = ["wav"] }
|
||||
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
|
||||
util.workspace = true
|
||||
|
||||
@@ -16,21 +16,16 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
SemanticVersion, Task, WindowContext,
|
||||
};
|
||||
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use paths::remote_servers_dir;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::{
|
||||
env::{
|
||||
self,
|
||||
@@ -32,24 +24,13 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::ResultExt;
|
||||
use which::which;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
ViewReleaseNotesLocally
|
||||
]
|
||||
);
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
@@ -146,12 +127,6 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
|
||||
|
||||
impl Global for GlobalAutoUpdate {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
@@ -161,10 +136,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
workspace.register_action(|_, action, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -264,121 +235,6 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
|
||||
None
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
|
||||
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(url) = url {
|
||||
cx.open_url(url);
|
||||
return;
|
||||
}
|
||||
|
||||
let version = AppVersion::global(cx).to_string();
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.build_url(&format!(
|
||||
"/api/release_notes/v2/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(view.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version;
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl AutoUpdater {
|
||||
pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
|
||||
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
||||
@@ -423,6 +279,10 @@ impl AutoUpdater {
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn current_version(&self) -> SemanticVersion {
|
||||
self.current_version
|
||||
}
|
||||
|
||||
pub fn status(&self) -> AutoUpdateStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
@@ -646,7 +506,7 @@ impl AutoUpdater {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_should_show_update_notification(
|
||||
pub fn set_should_show_update_notification(
|
||||
&self,
|
||||
should_show: bool,
|
||||
cx: &AppContext,
|
||||
@@ -668,7 +528,7 @@ impl AutoUpdater {
|
||||
})
|
||||
}
|
||||
|
||||
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
pub fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(KEY_VALUE_STORE
|
||||
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
|
||||
|
||||
28
crates/auto_update_ui/Cargo.toml
Normal file
28
crates/auto_update_ui/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "auto_update_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/auto_update_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
auto_update.workspace = true
|
||||
client.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/auto_update_ui/LICENSE-GPL
Symbolic link
1
crates/auto_update_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
147
crates/auto_update_ui/src/auto_update_ui.rs
Normal file
147
crates/auto_update_ui/src/auto_update_ui.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
mod update_notification;
|
||||
|
||||
use auto_update::AutoUpdater;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
|
||||
use http_client::HttpClient;
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use serde::Deserialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::update_notification::UpdateNotification;
|
||||
|
||||
actions!(auto_update, [ViewReleaseNotesLocally]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
|
||||
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(url) = url {
|
||||
cx.open_url(url);
|
||||
return;
|
||||
}
|
||||
|
||||
let version = AppVersion::global(cx).to_string();
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.build_url(&format!(
|
||||
"/api/release_notes/v2/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(view.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version();
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
None
|
||||
}
|
||||
@@ -16,11 +16,15 @@ doctest = false
|
||||
name = "cli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
no-bundled-uninstall = []
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
collections.workspace = true
|
||||
ipc-channel = "0.18"
|
||||
ipc-channel = "0.19"
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
|
||||
5
crates/cli/build.rs
Normal file
5
crates/cli/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
if std::env::var("ZED_UPDATE_EXPLANATION").is_ok() {
|
||||
println!(r#"cargo:rustc-cfg=feature="no-bundled-uninstall""#);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,13 @@ struct Args {
|
||||
/// Run zed in dev-server mode
|
||||
#[arg(long)]
|
||||
dev_server_token: Option<String>,
|
||||
/// Uninstall Zed from user system
|
||||
#[cfg(all(
|
||||
any(target_os = "linux", target_os = "macos"),
|
||||
not(feature = "no-bundled-uninstall")
|
||||
))]
|
||||
#[arg(long)]
|
||||
uninstall: bool,
|
||||
}
|
||||
|
||||
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||
@@ -119,6 +126,29 @@ fn main() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(target_os = "linux", target_os = "macos"),
|
||||
not(feature = "no-bundled-uninstall")
|
||||
))]
|
||||
if args.uninstall {
|
||||
static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");
|
||||
|
||||
let tmp_dir = tempfile::tempdir()?;
|
||||
let script_path = tmp_dir.path().join("uninstall.sh");
|
||||
fs::write(&script_path, UNINSTALL_SCRIPT)?;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
|
||||
|
||||
let status = std::process::Command::new("sh")
|
||||
.arg(&script_path)
|
||||
.env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME)
|
||||
.status()
|
||||
.context("Failed to execute uninstall script")?;
|
||||
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
@@ -1067,6 +1067,8 @@ impl Client {
|
||||
let proxy = http.proxy().cloned();
|
||||
let credentials = credentials.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
let system_id = self.telemetry.system_id();
|
||||
let metrics_id = self.telemetry.metrics_id();
|
||||
cx.background_executor().spawn(async move {
|
||||
use HttpOrHttps::*;
|
||||
|
||||
@@ -1118,6 +1120,12 @@ impl Client {
|
||||
"x-zed-release-channel",
|
||||
HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||
);
|
||||
if let Some(system_id) = system_id {
|
||||
request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?);
|
||||
}
|
||||
if let Some(metrics_id) = metrics_id {
|
||||
request_headers.insert("x-zed-metrics-id", HeaderValue::from_str(&metrics_id)?);
|
||||
}
|
||||
|
||||
match url_scheme {
|
||||
Https => {
|
||||
|
||||
@@ -224,6 +224,8 @@ impl Telemetry {
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let state = state.clone();
|
||||
let os_version = os_version();
|
||||
state.lock().os_version = Some(os_version.clone());
|
||||
async move {
|
||||
if let Some(tempfile) = File::create(Self::log_file_path()).log_err() {
|
||||
state.lock().log_file = Some(tempfile);
|
||||
@@ -531,6 +533,10 @@ impl Telemetry {
|
||||
self.state.lock().metrics_id.clone()
|
||||
}
|
||||
|
||||
pub fn system_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().system_id.clone()
|
||||
}
|
||||
|
||||
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().installation_id.clone()
|
||||
}
|
||||
|
||||
@@ -79,7 +79,8 @@ uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
context_servers.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
context_server.workspace = true
|
||||
async-trait.workspace = true
|
||||
audio.workspace = true
|
||||
call = { workspace = true, features = ["test-support"] }
|
||||
@@ -90,6 +91,7 @@ collections = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
extension.workspace = true
|
||||
file_finder.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
git = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -61,6 +61,39 @@ impl std::fmt::Display for CloudflareIpCountryHeader {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SystemIdHeader(String);
|
||||
|
||||
impl Header for SystemIdHeader {
|
||||
fn name() -> &'static HeaderName {
|
||||
static SYSTEM_ID_HEADER: OnceLock<HeaderName> = OnceLock::new();
|
||||
SYSTEM_ID_HEADER.get_or_init(|| HeaderName::from_static("x-zed-system-id"))
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
I: Iterator<Item = &'i axum::http::HeaderValue>,
|
||||
{
|
||||
let system_id = values
|
||||
.next()
|
||||
.ok_or_else(axum::headers::Error::invalid)?
|
||||
.to_str()
|
||||
.map_err(|_| axum::headers::Error::invalid())?;
|
||||
|
||||
Ok(Self(system_id.to_string()))
|
||||
}
|
||||
|
||||
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SystemIdHeader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/user", get(get_authenticated_user))
|
||||
|
||||
@@ -1420,8 +1420,6 @@ fn for_snowflake(
|
||||
"enable screen share" => "Screen Share Enabled".to_string(),
|
||||
"disable screen share" => "Screen Share Disabled".to_string(),
|
||||
"decline incoming" => "Incoming Call Declined".to_string(),
|
||||
"enable camera" => "Camera Enabled".to_string(),
|
||||
"disable camera" => "Camera Disabled".to_string(),
|
||||
_ => format!("Unknown Call Event: {}", e.operation),
|
||||
};
|
||||
|
||||
@@ -1444,65 +1442,64 @@ fn for_snowflake(
|
||||
Event::App(e) => {
|
||||
let mut properties = json!({});
|
||||
let event_type = match e.operation.trim() {
|
||||
"extensions: install extension" => "Extension Installed".to_string(),
|
||||
// App
|
||||
"open" => "App Opened".to_string(),
|
||||
"project search: open" => "Project Search Opened".to_string(),
|
||||
"first open" => {
|
||||
properties["is_first_open"] = json!(true);
|
||||
"App First Opened".to_string()
|
||||
}
|
||||
"extensions: uninstall extension" => "Extension Uninstalled".to_string(),
|
||||
"welcome page: close" => "Welcome Page Closed".to_string(),
|
||||
"open project" => {
|
||||
properties["is_first_time"] = json!(false);
|
||||
"Project Opened".to_string()
|
||||
}
|
||||
"welcome page: install cli" => "CLI Installed".to_string(),
|
||||
"project diagnostics: open" => "Project Diagnostics Opened".to_string(),
|
||||
"extensions page: open" => "Extensions Page Opened".to_string(),
|
||||
"welcome page: change theme" => "Welcome Theme Changed".to_string(),
|
||||
"welcome page: toggle metric telemetry" => {
|
||||
properties["enabled"] = json!(false);
|
||||
"Welcome Telemetry Toggled".to_string()
|
||||
}
|
||||
"welcome page: change keymap" => "Keymap Changed".to_string(),
|
||||
"welcome page: toggle vim" => {
|
||||
properties["enabled"] = json!(false);
|
||||
"Welcome Vim Mode Toggled".to_string()
|
||||
}
|
||||
"welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
|
||||
"welcome page: toggle diagnostic telemetry" => {
|
||||
"Welcome Telemetry Toggled".to_string()
|
||||
}
|
||||
"welcome page: open" => "Welcome Page Opened".to_string(),
|
||||
"close" => "App Closed".to_string(),
|
||||
"markdown preview: open" => "Markdown Preview Opened".to_string(),
|
||||
"welcome page: open extensions" => "Extensions Page Opened".to_string(),
|
||||
"open node project" | "open pnpm project" | "open yarn project" => {
|
||||
properties["project_type"] = json!("node");
|
||||
properties["is_first_time"] = json!(false);
|
||||
"Project Opened".to_string()
|
||||
}
|
||||
"repl sessions: open" => "REPL Session Started".to_string(),
|
||||
"welcome page: toggle helix" => {
|
||||
properties["enabled"] = json!(false);
|
||||
"Helix Mode Toggled".to_string()
|
||||
}
|
||||
"welcome page: edit settings" => {
|
||||
properties["changed_settings"] = json!([]);
|
||||
"Settings Edited".to_string()
|
||||
}
|
||||
"welcome page: view docs" => "Documentation Viewed".to_string(),
|
||||
"open ssh project" => {
|
||||
properties["is_first_time"] = json!(false);
|
||||
"SSH Project Opened".to_string()
|
||||
}
|
||||
"create ssh server" => "SSH Server Created".to_string(),
|
||||
"create ssh project" => "SSH Project Created".to_string(),
|
||||
"first open" => "App First Opened".to_string(),
|
||||
"first open for release channel" => {
|
||||
properties["is_first_for_channel"] = json!(true);
|
||||
"App First Opened For Release Channel".to_string()
|
||||
}
|
||||
"close" => "App Closed".to_string(),
|
||||
|
||||
// Project
|
||||
"open project" => "Project Opened".to_string(),
|
||||
"open node project" => {
|
||||
properties["project_type"] = json!("node");
|
||||
"Project Opened".to_string()
|
||||
}
|
||||
"open pnpm project" => {
|
||||
properties["project_type"] = json!("pnpm");
|
||||
"Project Opened".to_string()
|
||||
}
|
||||
"open yarn project" => {
|
||||
properties["project_type"] = json!("yarn");
|
||||
"Project Opened".to_string()
|
||||
}
|
||||
|
||||
// SSH
|
||||
"create ssh server" => "SSH Server Created".to_string(),
|
||||
"create ssh project" => "SSH Project Created".to_string(),
|
||||
"open ssh project" => "SSH Project Opened".to_string(),
|
||||
|
||||
// Welcome Page
|
||||
"welcome page: change keymap" => "Welcome Keymap Changed".to_string(),
|
||||
"welcome page: change theme" => "Welcome Theme Changed".to_string(),
|
||||
"welcome page: close" => "Welcome Page Closed".to_string(),
|
||||
"welcome page: edit settings" => "Welcome Settings Edited".to_string(),
|
||||
"welcome page: install cli" => "Welcome CLI Installed".to_string(),
|
||||
"welcome page: open" => "Welcome Page Opened".to_string(),
|
||||
"welcome page: open extensions" => "Welcome Extensions Page Opened".to_string(),
|
||||
"welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
|
||||
"welcome page: toggle diagnostic telemetry" => {
|
||||
"Welcome Diagnostic Telemetry Toggled".to_string()
|
||||
}
|
||||
"welcome page: toggle metric telemetry" => {
|
||||
"Welcome Metric Telemetry Toggled".to_string()
|
||||
}
|
||||
"welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(),
|
||||
"welcome page: view docs" => "Welcome Documentation Viewed".to_string(),
|
||||
|
||||
// Extensions
|
||||
"extensions page: open" => "Extensions Page Opened".to_string(),
|
||||
"extensions: install extension" => "Extension Installed".to_string(),
|
||||
"extensions: uninstall extension" => "Extension Uninstalled".to_string(),
|
||||
|
||||
// Misc
|
||||
"markdown preview: open" => "Markdown Preview Opened".to_string(),
|
||||
"project diagnostics: open" => "Project Diagnostics Opened".to_string(),
|
||||
"project search: open" => "Project Search Opened".to_string(),
|
||||
"repl sessions: open" => "REPL Session Started".to_string(),
|
||||
|
||||
// Feature Upsell
|
||||
"feature upsell: toggle vim" => {
|
||||
properties["source"] = json!("Feature Upsell");
|
||||
"Vim Mode Toggled".to_string()
|
||||
@@ -1555,10 +1552,13 @@ fn for_snowflake(
|
||||
);
|
||||
map.insert("signed_in".to_string(), event.signed_in.into());
|
||||
if let Some(country_code) = country_code.as_ref() {
|
||||
map.insert("country_code".to_string(), country_code.clone().into());
|
||||
map.insert("country".to_string(), country_code.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: most amplitude user properties are read out of our event_properties
|
||||
// dictionary. See https://app.amplitude.com/data/zed/Zed/sources/detail/production/falcon%3A159998
|
||||
// for how that is configured.
|
||||
let user_properties = Some(serde_json::json!({
|
||||
"is_staff": body.is_staff,
|
||||
}));
|
||||
@@ -1575,8 +1575,8 @@ fn for_snowflake(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SnowflakeRow {
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SnowflakeRow {
|
||||
pub time: chrono::DateTime<chrono::Utc>,
|
||||
pub user_id: Option<String>,
|
||||
pub device_id: Option<String>,
|
||||
@@ -1586,47 +1586,41 @@ struct SnowflakeRow {
|
||||
pub insert_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SnowflakeData {
|
||||
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
|
||||
pub installation_id: Option<String>,
|
||||
/// Identifier unique to each logged in Zed user (randomly generated on first sign in)
|
||||
/// Identifier unique to each Zed session (differs for each time you open Zed)
|
||||
pub session_id: Option<String>,
|
||||
pub metrics_id: Option<String>,
|
||||
/// True for Zed staff, otherwise false
|
||||
pub is_staff: Option<bool>,
|
||||
/// Zed version number
|
||||
pub app_version: String,
|
||||
pub os_name: String,
|
||||
pub os_version: Option<String>,
|
||||
pub architecture: String,
|
||||
/// Zed release channel (stable, preview, dev)
|
||||
pub release_channel: Option<String>,
|
||||
pub signed_in: bool,
|
||||
impl SnowflakeRow {
|
||||
pub fn new(
|
||||
event_type: impl Into<String>,
|
||||
metrics_id: Option<Uuid>,
|
||||
is_staff: bool,
|
||||
system_id: Option<String>,
|
||||
event_properties: serde_json::Value,
|
||||
) -> Self {
|
||||
Self {
|
||||
time: chrono::Utc::now(),
|
||||
event_type: event_type.into(),
|
||||
device_id: system_id,
|
||||
user_id: metrics_id.map(|id| id.to_string()),
|
||||
insert_id: Some(uuid::Uuid::new_v4().to_string()),
|
||||
event_properties,
|
||||
user_properties: Some(json!({"is_staff": is_staff})),
|
||||
}
|
||||
}
|
||||
|
||||
#[serde(flatten)]
|
||||
pub editor_event: Option<EditorEvent>,
|
||||
#[serde(flatten)]
|
||||
pub inline_completion_event: Option<InlineCompletionEvent>,
|
||||
#[serde(flatten)]
|
||||
pub call_event: Option<CallEvent>,
|
||||
#[serde(flatten)]
|
||||
pub assistant_event: Option<AssistantEvent>,
|
||||
#[serde(flatten)]
|
||||
pub cpu_event: Option<CpuEvent>,
|
||||
#[serde(flatten)]
|
||||
pub memory_event: Option<MemoryEvent>,
|
||||
#[serde(flatten)]
|
||||
pub app_event: Option<AppEvent>,
|
||||
#[serde(flatten)]
|
||||
pub setting_event: Option<SettingEvent>,
|
||||
#[serde(flatten)]
|
||||
pub extension_event: Option<ExtensionEvent>,
|
||||
#[serde(flatten)]
|
||||
pub edit_event: Option<EditEvent>,
|
||||
#[serde(flatten)]
|
||||
pub repl_event: Option<ReplEvent>,
|
||||
#[serde(flatten)]
|
||||
pub action_event: Option<ActionEvent>,
|
||||
pub async fn write(
|
||||
self,
|
||||
client: &Option<aws_sdk_kinesis::Client>,
|
||||
stream: &Option<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some((client, stream)) = client.as_ref().zip(stream.as_ref()) else {
|
||||
return Ok(());
|
||||
};
|
||||
let row = serde_json::to_vec(&self)?;
|
||||
client
|
||||
.put_record()
|
||||
.stream_name(stream)
|
||||
.partition_key(&self.user_id.unwrap_or_default())
|
||||
.data(row.into())
|
||||
.send()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use serde::Serialize;
|
||||
|
||||
/// A number of cents.
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -12,6 +14,7 @@
|
||||
derive_more::AddAssign,
|
||||
derive_more::Sub,
|
||||
derive_more::SubAssign,
|
||||
Serialize,
|
||||
)]
|
||||
pub struct Cents(pub u32);
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ pub mod db;
|
||||
mod telemetry;
|
||||
mod token;
|
||||
|
||||
use crate::api::events::SnowflakeRow;
|
||||
use crate::api::CloudflareIpCountryHeader;
|
||||
use crate::build_kinesis_client;
|
||||
use crate::{
|
||||
api::CloudflareIpCountryHeader, build_clickhouse_client, db::UserId, executor::Executor, Cents,
|
||||
Config, Error, Result,
|
||||
build_clickhouse_client, db::UserId, executor::Executor, Cents, Config, Error, Result,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use authorization::authorize_access_to_language_model;
|
||||
@@ -28,6 +30,7 @@ use rpc::{
|
||||
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
};
|
||||
use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
@@ -45,6 +48,7 @@ pub struct LlmState {
|
||||
pub executor: Executor,
|
||||
pub db: Arc<LlmDatabase>,
|
||||
pub http_client: ReqwestClient,
|
||||
pub kinesis_client: Option<aws_sdk_kinesis::Client>,
|
||||
pub clickhouse_client: Option<clickhouse::Client>,
|
||||
active_user_count_by_model:
|
||||
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
|
||||
@@ -77,6 +81,11 @@ impl LlmState {
|
||||
executor,
|
||||
db,
|
||||
http_client,
|
||||
kinesis_client: if config.kinesis_access_key.is_some() {
|
||||
build_kinesis_client(&config).await.log_err()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
clickhouse_client: config
|
||||
.clickhouse_url
|
||||
.as_ref()
|
||||
@@ -521,25 +530,50 @@ async fn check_usage_limit(
|
||||
UsageMeasure::TokensPerDay => "tokens_per_day",
|
||||
};
|
||||
|
||||
if let Some(client) = state.clickhouse_client.as_ref() {
|
||||
tracing::info!(
|
||||
target: "user rate limit",
|
||||
user_id = claims.user_id,
|
||||
login = claims.github_user_login,
|
||||
authn.jti = claims.jti,
|
||||
is_staff = claims.is_staff,
|
||||
provider = provider.to_string(),
|
||||
model = model.name,
|
||||
requests_this_minute = usage.requests_this_minute,
|
||||
tokens_this_minute = usage.tokens_this_minute,
|
||||
tokens_this_day = usage.tokens_this_day,
|
||||
users_in_recent_minutes = users_in_recent_minutes,
|
||||
users_in_recent_days = users_in_recent_days,
|
||||
max_requests_per_minute = per_user_max_requests_per_minute,
|
||||
max_tokens_per_minute = per_user_max_tokens_per_minute,
|
||||
max_tokens_per_day = per_user_max_tokens_per_day,
|
||||
);
|
||||
tracing::info!(
|
||||
target: "user rate limit",
|
||||
user_id = claims.user_id,
|
||||
login = claims.github_user_login,
|
||||
authn.jti = claims.jti,
|
||||
is_staff = claims.is_staff,
|
||||
provider = provider.to_string(),
|
||||
model = model.name,
|
||||
requests_this_minute = usage.requests_this_minute,
|
||||
tokens_this_minute = usage.tokens_this_minute,
|
||||
tokens_this_day = usage.tokens_this_day,
|
||||
users_in_recent_minutes = users_in_recent_minutes,
|
||||
users_in_recent_days = users_in_recent_days,
|
||||
max_requests_per_minute = per_user_max_requests_per_minute,
|
||||
max_tokens_per_minute = per_user_max_tokens_per_minute,
|
||||
max_tokens_per_day = per_user_max_tokens_per_day,
|
||||
);
|
||||
|
||||
SnowflakeRow::new(
|
||||
"Language Model Rate Limited",
|
||||
claims.metrics_id,
|
||||
claims.is_staff,
|
||||
claims.system_id.clone(),
|
||||
json!({
|
||||
"usage": usage,
|
||||
"users_in_recent_minutes": users_in_recent_minutes,
|
||||
"users_in_recent_days": users_in_recent_days,
|
||||
"max_requests_per_minute": per_user_max_requests_per_minute,
|
||||
"max_tokens_per_minute": per_user_max_tokens_per_minute,
|
||||
"max_tokens_per_day": per_user_max_tokens_per_day,
|
||||
"plan": match claims.plan {
|
||||
Plan::Free => "free".to_string(),
|
||||
Plan::ZedPro => "zed_pro".to_string(),
|
||||
},
|
||||
"model": model.name.clone(),
|
||||
"provider": provider.to_string(),
|
||||
"usage_measure": resource.to_string(),
|
||||
}),
|
||||
)
|
||||
.write(&state.kinesis_client, &state.config.kinesis_stream)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
if let Some(client) = state.clickhouse_client.as_ref() {
|
||||
report_llm_rate_limit(
|
||||
client,
|
||||
LlmRateLimitEventRow {
|
||||
@@ -652,6 +686,27 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
tokens_this_minute = usage.tokens_this_minute,
|
||||
);
|
||||
|
||||
let properties = json!({
|
||||
"plan": match claims.plan {
|
||||
Plan::Free => "free".to_string(),
|
||||
Plan::ZedPro => "zed_pro".to_string(),
|
||||
},
|
||||
"model": model,
|
||||
"provider": provider,
|
||||
"usage": usage,
|
||||
"tokens": tokens
|
||||
});
|
||||
SnowflakeRow::new(
|
||||
"Language Model Used",
|
||||
claims.metrics_id,
|
||||
claims.is_staff,
|
||||
claims.system_id.clone(),
|
||||
properties,
|
||||
)
|
||||
.write(&state.kinesis_client, &state.config.kinesis_stream)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
if let Some(clickhouse_client) = state.clickhouse_client.as_ref() {
|
||||
report_llm_usage(
|
||||
clickhouse_client,
|
||||
|
||||
@@ -9,7 +9,7 @@ use strum::IntoEnumIterator as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Default)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Default, serde::Serialize)]
|
||||
pub struct TokenUsage {
|
||||
pub input: usize,
|
||||
pub input_cache_creation: usize,
|
||||
@@ -23,7 +23,7 @@ impl TokenUsage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize)]
|
||||
pub struct Usage {
|
||||
pub requests_this_minute: usize,
|
||||
pub tokens_this_minute: usize,
|
||||
|
||||
@@ -8,6 +8,7 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -16,6 +17,10 @@ pub struct LlmTokenClaims {
|
||||
pub exp: u64,
|
||||
pub jti: String,
|
||||
pub user_id: u64,
|
||||
#[serde(default)]
|
||||
pub system_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub metrics_id: Option<Uuid>,
|
||||
pub github_user_login: String,
|
||||
pub is_staff: bool,
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
@@ -36,6 +41,7 @@ impl LlmTokenClaims {
|
||||
has_llm_closed_beta_feature_flag: bool,
|
||||
has_llm_subscription: bool,
|
||||
plan: rpc::proto::Plan,
|
||||
system_id: Option<String>,
|
||||
config: &Config,
|
||||
) -> Result<String> {
|
||||
let secret = config
|
||||
@@ -49,6 +55,8 @@ impl LlmTokenClaims {
|
||||
exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64,
|
||||
jti: uuid::Uuid::new_v4().to_string(),
|
||||
user_id: user.id.to_proto(),
|
||||
system_id,
|
||||
metrics_id: Some(user.metrics_id),
|
||||
github_user_login: user.github_login.clone(),
|
||||
is_staff,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod connection_pool;
|
||||
|
||||
use crate::api::CloudflareIpCountryHeader;
|
||||
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
||||
use crate::llm::LlmTokenClaims;
|
||||
use crate::{
|
||||
auth,
|
||||
@@ -137,6 +137,7 @@ struct Session {
|
||||
/// The GeoIP country code for the user.
|
||||
#[allow(unused)]
|
||||
geoip_country_code: Option<String>,
|
||||
system_id: Option<String>,
|
||||
_executor: Executor,
|
||||
}
|
||||
|
||||
@@ -682,6 +683,7 @@ impl Server {
|
||||
principal: Principal,
|
||||
zed_version: ZedVersion,
|
||||
geoip_country_code: Option<String>,
|
||||
system_id: Option<String>,
|
||||
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
executor: Executor,
|
||||
) -> impl Future<Output = ()> {
|
||||
@@ -737,6 +739,7 @@ impl Server {
|
||||
app_state: this.app_state.clone(),
|
||||
http_client,
|
||||
geoip_country_code,
|
||||
system_id,
|
||||
_executor: executor.clone(),
|
||||
supermaven_client,
|
||||
};
|
||||
@@ -1056,6 +1059,7 @@ pub fn routes(server: Arc<Server>) -> Router<(), Body> {
|
||||
.layer(Extension(server))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn handle_websocket_request(
|
||||
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
|
||||
app_version_header: Option<TypedHeader<AppVersionHeader>>,
|
||||
@@ -1063,6 +1067,7 @@ pub async fn handle_websocket_request(
|
||||
Extension(server): Extension<Arc<Server>>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||
system_id_header: Option<TypedHeader<SystemIdHeader>>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> axum::response::Response {
|
||||
if protocol_version != rpc::PROTOCOL_VERSION {
|
||||
@@ -1104,6 +1109,7 @@ pub async fn handle_websocket_request(
|
||||
principal,
|
||||
version,
|
||||
country_code_header.map(|header| header.to_string()),
|
||||
system_id_header.map(|header| header.to_string()),
|
||||
None,
|
||||
Executor::Production,
|
||||
)
|
||||
@@ -4053,6 +4059,7 @@ async fn get_llm_api_token(
|
||||
has_llm_closed_beta_feature_flag,
|
||||
has_llm_subscription,
|
||||
session.current_plan(&db).await?,
|
||||
session.system_id.clone(),
|
||||
&session.app_state.config,
|
||||
)?;
|
||||
response.send(proto::GetLlmTokenResponse { token })?;
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet, ToolWorkingSet};
|
||||
use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{User, RECEIVE_TIMEOUT};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -6486,8 +6487,8 @@ async fn test_context_collaboration_with_reconnect(
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
cx_a.update(context_servers::init);
|
||||
cx_b.update(context_servers::init);
|
||||
cx_a.update(context_server::init);
|
||||
cx_b.update(context_server::init);
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context_store_a = cx_a
|
||||
.update(|cx| {
|
||||
|
||||
@@ -835,7 +835,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::CodeAction => project
|
||||
.code_actions(&buffer, offset..offset, cx)
|
||||
.code_actions(&buffer, offset..offset, None, cx)
|
||||
.map(|_| Ok(()))
|
||||
.boxed(),
|
||||
LspRequestKind::Definition => project
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::tests::TestServer;
|
||||
use call::ActiveCall;
|
||||
use collections::HashSet;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _};
|
||||
@@ -81,6 +82,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -243,6 +245,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -400,6 +403,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: NodeRuntime::unavailable(),
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -244,6 +244,7 @@ impl TestServer {
|
||||
Principal::User(user),
|
||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
None,
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
))
|
||||
|
||||
@@ -58,12 +58,11 @@ settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
theme.workspace = true
|
||||
time_format.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
title_bar.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
vcs_menu.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -33,7 +33,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
notification_panel::init(cx);
|
||||
notifications::init(app_state, cx);
|
||||
title_bar::init(cx);
|
||||
vcs_menu::init(cx);
|
||||
}
|
||||
|
||||
fn notification_window_options(
|
||||
|
||||
@@ -11,7 +11,7 @@ use command_palette_hooks::{
|
||||
};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -21,9 +21,7 @@ use settings::Settings;
|
||||
use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, WorkspaceSettings};
|
||||
use zed_actions::OpenZedUrl;
|
||||
|
||||
actions!(command_palette, [Toggle]);
|
||||
use zed_actions::{command_palette::Toggle, OpenZedUrl};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
client::init_settings(cx);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "context_servers"
|
||||
name = "context_server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
@@ -9,22 +9,26 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/context_servers.rs"
|
||||
path = "src/context_server.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_server_settings.workspace = true
|
||||
extension.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
ui.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/context_server/LICENSE-GPL
Symbolic link
1
crates/context_server/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -9,7 +9,7 @@ use serde_json::{value::RawValue, Value};
|
||||
use smol::{
|
||||
channel,
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::{self, Child},
|
||||
process::Child,
|
||||
};
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -152,7 +152,7 @@ impl Client {
|
||||
&binary.args
|
||||
);
|
||||
|
||||
let mut command = process::Command::new(&binary.executable);
|
||||
let mut command = util::command::new_smol_command(&binary.executable);
|
||||
command
|
||||
.args(&binary.args)
|
||||
.envs(binary.env.unwrap_or_default())
|
||||
@@ -1,14 +1,16 @@
|
||||
pub mod client;
|
||||
mod context_server_tool;
|
||||
mod extension_context_server;
|
||||
pub mod manager;
|
||||
pub mod protocol;
|
||||
mod registry;
|
||||
pub mod types;
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub use context_server_settings::{ContextServerSettings, ServerCommand, ServerConfig};
|
||||
use gpui::{actions, AppContext};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::manager::ContextServerSettings;
|
||||
pub use crate::context_server_tool::ContextServerTool;
|
||||
pub use crate::registry::ContextServerFactoryRegistry;
|
||||
|
||||
actions!(context_servers, [Restart]);
|
||||
@@ -17,8 +19,9 @@ actions!(context_servers, [Restart]);
|
||||
pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
ContextServerSettings::register(cx);
|
||||
context_server_settings::init(cx);
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
extension_context_server::init(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);
|
||||
@@ -2,10 +2,11 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use assistant_tool::Tool;
|
||||
use context_servers::manager::ContextServerManager;
|
||||
use context_servers::types;
|
||||
use gpui::{Model, Task};
|
||||
|
||||
use crate::manager::ContextServerManager;
|
||||
use crate::types;
|
||||
|
||||
pub struct ContextServerTool {
|
||||
server_manager: Model<ContextServerManager>,
|
||||
server_id: Arc<str>,
|
||||
77
crates/context_server/src/extension_context_server.rs
Normal file
77
crates/context_server/src/extension_context_server.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate};
|
||||
use gpui::{AppContext, Model};
|
||||
|
||||
use crate::{ContextServerFactoryRegistry, ServerCommand};
|
||||
|
||||
struct ExtensionProject {
|
||||
worktree_ids: Vec<u64>,
|
||||
}
|
||||
|
||||
impl ProjectDelegate for ExtensionProject {
|
||||
fn worktree_ids(&self) -> Vec<u64> {
|
||||
self.worktree_ids.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let proxy = ExtensionHostProxy::default_global(cx);
|
||||
proxy.register_context_server_proxy(ContextServerFactoryRegistryProxy {
|
||||
context_server_factory_registry: ContextServerFactoryRegistry::global(cx),
|
||||
});
|
||||
}
|
||||
|
||||
struct ContextServerFactoryRegistryProxy {
|
||||
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
|
||||
}
|
||||
|
||||
impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy {
|
||||
fn register_context_server(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
id: Arc<str>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.context_server_factory_registry
|
||||
.update(cx, |registry, _| {
|
||||
registry.register_server_factory(
|
||||
id.clone(),
|
||||
Arc::new({
|
||||
move |project, cx| {
|
||||
log::info!(
|
||||
"loading command for context server {id} from extension {}",
|
||||
extension.manifest().id
|
||||
);
|
||||
|
||||
let id = id.clone();
|
||||
let extension = extension.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let extension_project =
|
||||
project.update(&mut cx, |project, cx| {
|
||||
Arc::new(ExtensionProject {
|
||||
worktree_ids: project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).id().to_proto())
|
||||
.collect(),
|
||||
})
|
||||
})?;
|
||||
|
||||
let command = extension
|
||||
.context_server_command(id.clone(), extension_project)
|
||||
.await?;
|
||||
|
||||
log::info!("loaded command for context server {id}: {command:?}");
|
||||
|
||||
Ok(ServerCommand {
|
||||
path: command.command,
|
||||
args: command.args,
|
||||
env: Some(command.env.into_iter().collect()),
|
||||
})
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -24,48 +24,16 @@ use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Tas
|
||||
use log;
|
||||
use parking_lot::RwLock;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{ContextServerSettings, ServerConfig};
|
||||
|
||||
use crate::{
|
||||
client::{self, Client},
|
||||
types, ContextServerFactoryRegistry, CONTEXT_SERVERS_NAMESPACE,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
pub struct ContextServerSettings {
|
||||
#[serde(default)]
|
||||
pub context_servers: HashMap<Arc<str>, ServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
|
||||
pub struct ServerConfig {
|
||||
pub command: Option<ServerCommand>,
|
||||
pub settings: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
pub struct ServerCommand {
|
||||
pub path: String,
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl Settings for ContextServerSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextServer {
|
||||
pub id: Arc<str>,
|
||||
pub config: Arc<ServerConfig>,
|
||||
@@ -5,7 +5,7 @@ use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ReadGlobal, Task};
|
||||
use project::Project;
|
||||
|
||||
use crate::manager::ServerCommand;
|
||||
use crate::ServerCommand;
|
||||
|
||||
pub type ContextServerFactory = Arc<
|
||||
dyn Fn(Model<Project>, &AsyncAppContext) -> Task<Result<ServerCommand>> + Send + Sync + 'static,
|
||||
21
crates/context_server_settings/Cargo.toml
Normal file
21
crates/context_server_settings/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "context_server_settings"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/context_server_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
1
crates/context_server_settings/LICENSE-GPL
Symbolic link
1
crates/context_server_settings/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::AppContext;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
ContextServerSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
|
||||
pub struct ServerConfig {
|
||||
/// The command to run this context server.
|
||||
///
|
||||
/// This will override the command set by an extension.
|
||||
pub command: Option<ServerCommand>,
|
||||
/// The settings for this context server.
|
||||
///
|
||||
/// Consult the documentation for the context server to see what settings
|
||||
/// are supported.
|
||||
#[schemars(schema_with = "server_config_settings_json_schema")]
|
||||
pub settings: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
pub struct ServerCommand {
|
||||
pub path: String,
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
pub struct ContextServerSettings {
|
||||
/// Settings for context servers used in the Assistant.
|
||||
#[serde(default)]
|
||||
pub context_servers: HashMap<Arc<str>, ServerConfig>,
|
||||
}
|
||||
|
||||
impl Settings for ContextServerSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
@@ -29,14 +29,14 @@ anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
inline_completion.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
@@ -44,12 +44,12 @@ node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
task.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -38,8 +38,8 @@ use std::{
|
||||
};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
|
||||
pub use copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use sign_in::CopilotCodeVerification;
|
||||
pub use crate::copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use crate::sign_in::{initiate_sign_in, CopilotCodeVerification};
|
||||
|
||||
actions!(
|
||||
copilot,
|
||||
@@ -1231,7 +1231,7 @@ mod tests {
|
||||
|
||||
fn disk_state(&self) -> language::DiskState {
|
||||
language::DiskState::Present {
|
||||
mtime: std::time::UNIX_EPOCH,
|
||||
mtime: ::fs::MTime::from_seconds_and_nanos(100, 42),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut AppContext) {
|
||||
cx.set_global(GlobalCopilotChat(copilot_chat));
|
||||
}
|
||||
|
||||
fn copilot_chat_config_path() -> &'static PathBuf {
|
||||
fn copilot_chat_config_dir() -> &'static PathBuf {
|
||||
static COPILOT_CHAT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
COPILOT_CHAT_CONFIG_DIR.get_or_init(|| {
|
||||
@@ -207,10 +207,14 @@ fn copilot_chat_config_path() -> &'static PathBuf {
|
||||
home_dir().join(".config")
|
||||
}
|
||||
.join("github-copilot")
|
||||
.join("hosts.json")
|
||||
})
|
||||
}
|
||||
|
||||
fn copilot_chat_config_paths() -> [PathBuf; 2] {
|
||||
let base_dir = copilot_chat_config_dir();
|
||||
[base_dir.join("hosts.json"), base_dir.join("apps.json")]
|
||||
}
|
||||
|
||||
impl CopilotChat {
|
||||
pub fn global(cx: &AppContext) -> Option<gpui::Model<Self>> {
|
||||
cx.try_global::<GlobalCopilotChat>()
|
||||
@@ -218,13 +222,24 @@ impl CopilotChat {
|
||||
}
|
||||
|
||||
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &AppContext) -> Self {
|
||||
let mut config_file_rx = watch_config_file(
|
||||
cx.background_executor(),
|
||||
fs,
|
||||
copilot_chat_config_path().clone(),
|
||||
);
|
||||
let config_paths = copilot_chat_config_paths();
|
||||
|
||||
let resolve_config_path = {
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
for config_path in config_paths.iter() {
|
||||
if fs.metadata(config_path).await.is_ok_and(|v| v.is_some()) {
|
||||
return config_path.clone();
|
||||
}
|
||||
}
|
||||
config_paths[0].clone()
|
||||
}
|
||||
};
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
let config_file = resolve_config_path.await;
|
||||
let mut config_file_rx = watch_config_file(cx.background_executor(), fs, config_file);
|
||||
|
||||
while let Some(contents) = config_file_rx.next().await {
|
||||
let oauth_token = extract_oauth_token(contents);
|
||||
|
||||
@@ -318,9 +333,15 @@ async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Re
|
||||
fn extract_oauth_token(contents: String) -> Option<String> {
|
||||
serde_json::from_str::<serde_json::Value>(&contents)
|
||||
.map(|v| {
|
||||
v["github.com"]["oauth_token"]
|
||||
.as_str()
|
||||
.map(|v| v.to_string())
|
||||
v.as_object().and_then(|obj| {
|
||||
obj.iter().find_map(|(key, value)| {
|
||||
if key.starts_with("github.com") {
|
||||
value["oauth_token"].as_str().map(|v| v.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
|
||||
@@ -5,10 +5,79 @@ use gpui::{
|
||||
Styled, Subscription, ViewContext,
|
||||
};
|
||||
use ui::{prelude::*, Button, Label, Vector, VectorName};
|
||||
use workspace::ModalView;
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{ModalView, Toast, Workspace};
|
||||
|
||||
const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
|
||||
|
||||
struct CopilotStartingToast;
|
||||
|
||||
pub fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot is starting...",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot has started!",
|
||||
),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(
|
||||
&NotificationId::unique::<CopilotStartingToast>(),
|
||||
cx,
|
||||
);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
|
||||
workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
|
||||
@@ -33,6 +33,7 @@ use std::{
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
@@ -82,6 +83,8 @@ struct DiagnosticGroupState {
|
||||
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let child = if self.path_states.is_empty() {
|
||||
@@ -131,16 +134,27 @@ impl ProjectDiagnosticsEditor {
|
||||
language_server_id,
|
||||
path,
|
||||
} => {
|
||||
this.paths_to_update
|
||||
.insert((path.clone(), Some(*language_server_id)));
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
let max_severity = this.max_severity();
|
||||
let has_diagnostics_to_display = project.read(cx).lsp_store().read(cx).diagnostics_for_buffer(path)
|
||||
.into_iter().flatten()
|
||||
.filter(|(server_id, _)| language_server_id == server_id)
|
||||
.flat_map(|(_, diagnostics)| diagnostics)
|
||||
.any(|diagnostic| diagnostic.diagnostic.severity <= max_severity);
|
||||
|
||||
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
|
||||
if has_diagnostics_to_display {
|
||||
this.paths_to_update
|
||||
.insert((path.clone(), Some(*language_server_id)));
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
|
||||
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
|
||||
} else {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
|
||||
this.update_stale_excerpts(cx);
|
||||
}
|
||||
} else {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
|
||||
this.update_stale_excerpts(cx);
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. no diagnostics to display");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -198,6 +212,9 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
let project_handle = self.project.clone();
|
||||
self.update_excerpts_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
|
||||
.await;
|
||||
loop {
|
||||
let Some((path, language_server_id)) = this.update(&mut cx, |this, _| {
|
||||
let Some((path, language_server_id)) = this.paths_to_update.pop_first() else {
|
||||
@@ -323,16 +340,12 @@ impl ProjectDiagnosticsEditor {
|
||||
ExcerptId::min()
|
||||
};
|
||||
|
||||
let max_severity = self.max_severity();
|
||||
let path_state = &mut self.path_states[path_ix];
|
||||
let mut new_group_ixs = Vec::new();
|
||||
let mut blocks_to_add = Vec::new();
|
||||
let mut blocks_to_remove = HashSet::default();
|
||||
let mut first_excerpt_id = None;
|
||||
let max_severity = if self.include_warnings {
|
||||
DiagnosticSeverity::WARNING
|
||||
} else {
|
||||
DiagnosticSeverity::ERROR
|
||||
};
|
||||
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| {
|
||||
let mut old_groups = mem::take(&mut path_state.diagnostic_groups)
|
||||
.into_iter()
|
||||
@@ -621,6 +634,14 @@ impl ProjectDiagnosticsEditor {
|
||||
prev_path = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn max_severity(&self) -> DiagnosticSeverity {
|
||||
if self.include_warnings {
|
||||
DiagnosticSeverity::WARNING
|
||||
} else {
|
||||
DiagnosticSeverity::ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ProjectDiagnosticsEditor {
|
||||
@@ -695,7 +716,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &AppContext,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||
) {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
@@ -776,7 +797,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
});
|
||||
let editor = view.update(cx, |view, _| view.editor.clone());
|
||||
|
||||
view.next_notification(cx).await;
|
||||
view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
|
||||
.await;
|
||||
assert_eq!(
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
@@ -240,7 +241,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
|
||||
});
|
||||
|
||||
view.next_notification(cx).await;
|
||||
view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
|
||||
.await;
|
||||
assert_eq!(
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
@@ -352,7 +354,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
|
||||
});
|
||||
|
||||
view.next_notification(cx).await;
|
||||
view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
|
||||
.await;
|
||||
assert_eq!(
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
@@ -491,6 +494,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Only the first language server's diagnostics are shown.
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
editor_blocks(&editor, cx),
|
||||
@@ -537,6 +542,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Both language server's diagnostics are shown.
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
editor_blocks(&editor, cx),
|
||||
@@ -603,6 +610,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Only the first language server's diagnostics are updated.
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
editor_blocks(&editor, cx),
|
||||
@@ -659,6 +668,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Both language servers' diagnostics are updated.
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
editor_blocks(&editor, cx),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
|
||||
ViewContext, WeakView,
|
||||
EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
|
||||
WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
|
||||
@@ -77,8 +77,10 @@ impl Render for DiagnosticIndicator {
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.h(rems(1.375))
|
||||
.gap_2()
|
||||
.pl_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
ButtonLike::new("diagnostic-indicator")
|
||||
.child(diagnostic_indicator)
|
||||
|
||||
@@ -42,10 +42,12 @@ emojis.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
fs.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
inline_completion.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
|
||||
@@ -271,6 +271,8 @@ gpui::actions!(
|
||||
Hover,
|
||||
Indent,
|
||||
JoinLines,
|
||||
KillRingCut,
|
||||
KillRingYank,
|
||||
LineDown,
|
||||
LineUp,
|
||||
MoveDown,
|
||||
|
||||
@@ -1067,11 +1067,8 @@ impl DisplaySnapshot {
|
||||
self.block_snapshot.block_for_id(id)
|
||||
}
|
||||
|
||||
pub fn intersects_fold<T: ToPoint>(&self, point: T) -> bool {
|
||||
let point = point.to_point(&self.buffer_snapshot);
|
||||
self.block_snapshot
|
||||
.is_line_replaced(MultiBufferRow(point.row))
|
||||
|| self.fold_snapshot.intersects_fold(point)
|
||||
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
|
||||
self.fold_snapshot.intersects_fold(offset)
|
||||
}
|
||||
|
||||
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
|
||||
@@ -28,7 +28,6 @@ mod hover_popover;
|
||||
mod hunk_diff;
|
||||
mod indent_guides;
|
||||
mod inlay_hint_cache;
|
||||
mod inline_completion_provider;
|
||||
pub mod items;
|
||||
mod linked_editing_ranges;
|
||||
mod lsp_ext;
|
||||
@@ -75,7 +74,7 @@ use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
|
||||
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
|
||||
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
FocusableView, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render,
|
||||
ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
|
||||
@@ -87,7 +86,8 @@ pub(crate) use hunk_diff::HoveredHunk;
|
||||
use hunk_diff::{diff_hunk_to_display, ExpandedHunks};
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion_provider::*;
|
||||
pub use inline_completion::Direction;
|
||||
use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
@@ -125,8 +125,8 @@ use parking_lot::{Mutex, RwLock};
|
||||
use project::{
|
||||
lsp_store::{FormatTarget, FormatTrigger},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location,
|
||||
LocationLink, Project, ProjectTransaction, TaskSourceKind,
|
||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
|
||||
Project, ProjectItem, ProjectTransaction, TaskSourceKind,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::{proto::*, ErrorExt};
|
||||
@@ -273,12 +273,6 @@ enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
enum InputComposition {}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Prev,
|
||||
Next,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Navigated {
|
||||
Yes,
|
||||
@@ -540,15 +534,6 @@ pub enum IsVimMode {
|
||||
No,
|
||||
}
|
||||
|
||||
pub trait ActiveLineTrailerProvider {
|
||||
fn render_active_line_trailer(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
focus_handle: &FocusHandle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement>;
|
||||
}
|
||||
|
||||
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
|
||||
///
|
||||
/// See the [module level documentation](self) for more information.
|
||||
@@ -611,7 +596,6 @@ pub struct Editor {
|
||||
auto_signature_help: Option<bool>,
|
||||
find_all_references_task_sources: Vec<Anchor>,
|
||||
next_completion_id: CompletionId,
|
||||
completion_documentation_pre_resolve_debounce: DebouncedDelay,
|
||||
available_code_actions: Option<(Location, Arc<[AvailableCodeAction]>)>,
|
||||
code_actions_task: Option<Task<Result<()>>>,
|
||||
document_highlights_task: Option<Task<()>>,
|
||||
@@ -676,7 +660,6 @@ pub struct Editor {
|
||||
next_scroll_position: NextScrollCursorCenterTopBottom,
|
||||
addons: HashMap<TypeId, Box<dyn Addon>>,
|
||||
_scroll_cursor_center_top_bottom_task: Task<()>,
|
||||
active_line_trailer_provider: Option<Box<dyn ActiveLineTrailerProvider>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
@@ -1022,7 +1005,7 @@ struct CompletionsMenu {
|
||||
matches: Arc<[StringMatch]>,
|
||||
selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_completion_documentation_resolve_debounce: Option<Arc<Mutex<DebouncedDelay>>>,
|
||||
selected_completion_resolve_debounce: Option<Arc<Mutex<DebouncedDelay>>>,
|
||||
}
|
||||
|
||||
impl CompletionsMenu {
|
||||
@@ -1054,9 +1037,7 @@ impl CompletionsMenu {
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
|
||||
DebouncedDelay::new(),
|
||||
))),
|
||||
selected_completion_resolve_debounce: Some(Arc::new(Mutex::new(DebouncedDelay::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,15 +1090,12 @@ impl CompletionsMenu {
|
||||
matches,
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
|
||||
DebouncedDelay::new(),
|
||||
))),
|
||||
selected_completion_resolve_debounce: Some(Arc::new(Mutex::new(DebouncedDelay::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
fn suppress_documentation_resolution(mut self) -> Self {
|
||||
self.selected_completion_documentation_resolve_debounce
|
||||
.take();
|
||||
self.selected_completion_resolve_debounce.take();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1129,7 +1107,7 @@ impl CompletionsMenu {
|
||||
self.selected_item = 0;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.attempt_resolve_selected_completion_documentation(provider, cx);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1145,7 +1123,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.attempt_resolve_selected_completion_documentation(provider, cx);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1161,7 +1139,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.attempt_resolve_selected_completion_documentation(provider, cx);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1173,58 +1151,20 @@ impl CompletionsMenu {
|
||||
self.selected_item = self.matches.len() - 1;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.attempt_resolve_selected_completion_documentation(provider, cx);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn pre_resolve_completion_documentation(
|
||||
buffer: Model<Buffer>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
matches: Arc<[StringMatch]>,
|
||||
editor: &Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<()> {
|
||||
let settings = EditorSettings::get_global(cx);
|
||||
if !settings.show_completion_documentation {
|
||||
return Task::ready(());
|
||||
}
|
||||
|
||||
let Some(provider) = editor.completion_provider.as_ref() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let resolve_task = provider.resolve_completions(
|
||||
buffer,
|
||||
matches.iter().map(|m| m.candidate_id).collect(),
|
||||
completions.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn attempt_resolve_selected_completion_documentation(
|
||||
fn resolve_selected_completion(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let settings = EditorSettings::get_global(cx);
|
||||
if !settings.show_completion_documentation {
|
||||
return;
|
||||
}
|
||||
|
||||
let completion_index = self.matches[self.selected_item].candidate_id;
|
||||
let Some(provider) = provider else {
|
||||
return;
|
||||
};
|
||||
let Some(documentation_resolve) = self
|
||||
.selected_completion_documentation_resolve_debounce
|
||||
.as_ref()
|
||||
else {
|
||||
let Some(completion_resolve) = self.selected_completion_resolve_debounce.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1239,7 +1179,7 @@ impl CompletionsMenu {
|
||||
EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
|
||||
let delay = Duration::from_millis(delay_ms);
|
||||
|
||||
documentation_resolve.lock().fire_new(delay, cx, |_, cx| {
|
||||
completion_resolve.lock().fire_new(delay, cx, |_, cx| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
@@ -2134,7 +2074,6 @@ impl Editor {
|
||||
auto_signature_help: None,
|
||||
find_all_references_task_sources: Vec::new(),
|
||||
next_completion_id: 0,
|
||||
completion_documentation_pre_resolve_debounce: DebouncedDelay::new(),
|
||||
next_inlay_id: 0,
|
||||
code_action_providers,
|
||||
available_code_actions: Default::default(),
|
||||
@@ -2215,7 +2154,6 @@ impl Editor {
|
||||
addons: HashMap::default(),
|
||||
_scroll_cursor_center_top_bottom_task: Task::ready(()),
|
||||
text_style_refinement: None,
|
||||
active_line_trailer_provider: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
@@ -2504,16 +2442,6 @@ impl Editor {
|
||||
self.refresh_inline_completion(false, false, cx);
|
||||
}
|
||||
|
||||
pub fn set_active_line_trailer_provider<T>(
|
||||
&mut self,
|
||||
provider: Option<T>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
T: ActiveLineTrailerProvider + 'static,
|
||||
{
|
||||
self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>);
|
||||
}
|
||||
|
||||
pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> {
|
||||
self.placeholder_text.as_deref()
|
||||
}
|
||||
@@ -3003,7 +2931,7 @@ impl Editor {
|
||||
let start;
|
||||
let end;
|
||||
let mode;
|
||||
let auto_scroll;
|
||||
let mut auto_scroll;
|
||||
match click_count {
|
||||
1 => {
|
||||
start = buffer.anchor_before(position.to_point(&display_map));
|
||||
@@ -3039,6 +2967,7 @@ impl Editor {
|
||||
auto_scroll = false;
|
||||
}
|
||||
}
|
||||
auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks;
|
||||
|
||||
let point_to_delete: Option<usize> = {
|
||||
let selected_points: Vec<Selection<Point>> =
|
||||
@@ -4550,9 +4479,9 @@ impl Editor {
|
||||
let sort_completions = provider.sort_completions();
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
let task = cx.spawn(|editor, mut cx| {
|
||||
async move {
|
||||
this.update(&mut cx, |this, _| {
|
||||
editor.update(&mut cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
})?;
|
||||
let completions = completions.await.log_err();
|
||||
@@ -4570,34 +4499,14 @@ impl Editor {
|
||||
if menu.matches.is_empty() {
|
||||
None
|
||||
} else {
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
let completions = menu.completions.clone();
|
||||
let matches = menu.matches.clone();
|
||||
|
||||
let delay_ms = EditorSettings::get_global(cx)
|
||||
.completion_documentation_secondary_query_debounce;
|
||||
let delay = Duration::from_millis(delay_ms);
|
||||
editor
|
||||
.completion_documentation_pre_resolve_debounce
|
||||
.fire_new(delay, cx, |editor, cx| {
|
||||
CompletionsMenu::pre_resolve_completion_documentation(
|
||||
buffer,
|
||||
completions,
|
||||
matches,
|
||||
editor,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
Some(menu)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut context_menu = this.context_menu.write();
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let mut context_menu = editor.context_menu.write();
|
||||
match context_menu.as_ref() {
|
||||
None => {}
|
||||
|
||||
@@ -4610,19 +4519,20 @@ impl Editor {
|
||||
_ => return,
|
||||
}
|
||||
|
||||
if this.focus_handle.is_focused(cx) && menu.is_some() {
|
||||
let menu = menu.unwrap();
|
||||
if editor.focus_handle.is_focused(cx) && menu.is_some() {
|
||||
let mut menu = menu.unwrap();
|
||||
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
|
||||
*context_menu = Some(ContextMenu::Completions(menu));
|
||||
drop(context_menu);
|
||||
this.discard_inline_completion(false, cx);
|
||||
editor.discard_inline_completion(false, cx);
|
||||
cx.notify();
|
||||
} else if this.completion_tasks.len() <= 1 {
|
||||
} else if editor.completion_tasks.len() <= 1 {
|
||||
// If there are no more completion tasks and the last menu was
|
||||
// empty, we should hide it. If it was already hidden, we should
|
||||
// also show the copilot completion when available.
|
||||
drop(context_menu);
|
||||
if this.hide_context_menu(cx).is_none() {
|
||||
this.update_visible_inline_completion(cx);
|
||||
if editor.hide_context_menu(cx).is_none() {
|
||||
editor.update_visible_inline_completion(cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
@@ -7370,7 +7280,7 @@ impl Editor {
|
||||
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
||||
}
|
||||
|
||||
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
||||
pub fn cut_common(&mut self, cx: &mut ViewContext<Self>) -> ClipboardItem {
|
||||
let mut text = String::new();
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let mut selections = self.selections.all::<Point>(cx);
|
||||
@@ -7414,11 +7324,38 @@ impl Editor {
|
||||
s.select(selections);
|
||||
});
|
||||
this.insert("", cx);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
|
||||
text,
|
||||
clipboard_selections,
|
||||
));
|
||||
});
|
||||
ClipboardItem::new_string_with_json_metadata(text, clipboard_selections)
|
||||
}
|
||||
|
||||
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
||||
let item = self.cut_common(cx);
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
pub fn kill_ring_cut(&mut self, _: &KillRingCut, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(None, cx, |s| {
|
||||
s.move_with(|snapshot, sel| {
|
||||
if sel.is_empty() {
|
||||
sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row()))
|
||||
}
|
||||
});
|
||||
});
|
||||
let item = self.cut_common(cx);
|
||||
cx.set_global(KillRing(item))
|
||||
}
|
||||
|
||||
pub fn kill_ring_yank(&mut self, _: &KillRingYank, cx: &mut ViewContext<Self>) {
|
||||
let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() {
|
||||
if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() {
|
||||
(kill_ring.text().to_string(), kill_ring.metadata_json())
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
self.do_paste(&text, metadata, false, cx);
|
||||
}
|
||||
|
||||
pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
@@ -11859,6 +11796,10 @@ impl Editor {
|
||||
self.blame.as_ref()
|
||||
}
|
||||
|
||||
pub fn show_git_blame_gutter(&self) -> bool {
|
||||
self.show_git_blame_gutter
|
||||
}
|
||||
|
||||
pub fn render_git_blame_gutter(&mut self, cx: &mut WindowContext) -> bool {
|
||||
self.show_git_blame_gutter && self.has_blame_entries(cx)
|
||||
}
|
||||
@@ -11870,21 +11811,6 @@ impl Editor {
|
||||
&& self.has_blame_entries(cx)
|
||||
}
|
||||
|
||||
pub fn render_active_line_trailer(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
self.active_line_trailer_provider
|
||||
.as_mut()?
|
||||
.render_active_line_trailer(style, &focus_handle, cx)
|
||||
}
|
||||
|
||||
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
|
||||
self.blame()
|
||||
.map_or(false, |blame| blame.read(cx).has_generated_entries())
|
||||
@@ -13782,7 +13708,9 @@ impl CodeActionProvider for Model<Project> {
|
||||
range: Range<text::Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
self.update(cx, |project, cx| project.code_actions(buffer, range, cx))
|
||||
self.update(cx, |project, cx| {
|
||||
project.code_actions(buffer, range, None, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_code_action(
|
||||
@@ -14426,15 +14354,16 @@ impl ViewInputHandler for Editor {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
Some(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.read(cx)
|
||||
.text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
|
||||
.collect(),
|
||||
)
|
||||
let snapshot = self.buffer.read(cx).read(cx);
|
||||
let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left);
|
||||
let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right);
|
||||
if (start.0..end.0) != range_utf16 {
|
||||
adjusted_range.replace(start.0..end.0);
|
||||
}
|
||||
Some(snapshot.text_for_range(start..end).collect())
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
@@ -14682,7 +14611,8 @@ impl ViewInputHandler for Editor {
|
||||
|
||||
let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot);
|
||||
let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left
|
||||
+ self.gutter_dimensions.width;
|
||||
+ self.gutter_dimensions.width
|
||||
+ self.gutter_dimensions.margin;
|
||||
let y = line_height * (start.row().as_f32() - scroll_position.y);
|
||||
|
||||
Some(Bounds {
|
||||
@@ -15142,4 +15072,7 @@ fn check_multiline_range(buffer: &Buffer, range: Range<usize>) -> Range<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KillRing(ClipboardItem);
|
||||
impl Global for KillRing {}
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct EditorSettings {
|
||||
pub gutter: Gutter,
|
||||
pub scroll_beyond_last_line: ScrollBeyondLastLine,
|
||||
pub vertical_scroll_margin: f32,
|
||||
pub autoscroll_on_clicks: bool,
|
||||
pub scroll_sensitivity: f32,
|
||||
pub relative_line_numbers: bool,
|
||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||
@@ -222,6 +223,10 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: 3.
|
||||
pub vertical_scroll_margin: Option<f32>,
|
||||
/// Whether to scroll when clicking near the edge of the visible text area.
|
||||
///
|
||||
/// Default: false
|
||||
pub autoscroll_on_clicks: Option<bool>,
|
||||
/// Scroll sensitivity multiplier. This multiplier is applied
|
||||
/// to both the horizontal and vertical delta values while scrolling.
|
||||
///
|
||||
|
||||
@@ -31,8 +31,8 @@ use project::{
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::sync::atomic;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
@@ -10541,6 +10541,221 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
||||
cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
|
||||
cx.simulate_keystroke(".");
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "very": "special"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
|
||||
let expect_first_item = Arc::new(AtomicBool::new(true));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_default_data = default_data.clone();
|
||||
let closure_resolve_requests_number = resolve_requests_number.clone();
|
||||
let closure_expect_first_item = expect_first_item.clone();
|
||||
let closure_default_commit_characters = default_commit_characters.clone();
|
||||
move |item_to_resolve, _| {
|
||||
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
|
||||
let default_data = closure_default_data.clone();
|
||||
let default_commit_characters = closure_default_commit_characters.clone();
|
||||
let expect_first_item = closure_expect_first_item.clone();
|
||||
async move {
|
||||
if expect_first_item.load(atomic::Ordering::Acquire) {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "Some(2)",
|
||||
"Should have selected the first item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(json!({ "very": "special"})),
|
||||
"First item should bring its own data for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"First item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
|
||||
),
|
||||
"First item should bring its own edit range for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(default_insert_text_format),
|
||||
"First item had no own insert text format and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
"First item should bring its own insert text mode for resolving"
|
||||
);
|
||||
Ok(item_to_resolve)
|
||||
} else {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "vec![2]",
|
||||
"Should have selected the last item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(default_data),
|
||||
"Last item has no own resolve data and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"Last item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: "vec![2]".to_string()
|
||||
})),
|
||||
"Last item had no own edit range and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
"Last item should bring its own insert text format for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(default_insert_text_mode),
|
||||
"Last item had no own insert text mode and should inherit the default one"
|
||||
);
|
||||
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
let completion_data = default_data.clone();
|
||||
let completion_characters = default_commit_characters.clone();
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let default_data = completion_data.clone();
|
||||
let default_commit_characters = completion_characters.clone();
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
items: vec![
|
||||
lsp::CompletionItem {
|
||||
label: "Some(2)".into(),
|
||||
insert_text: Some("Some(2)".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "Some(2)".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "vec![2]".into(),
|
||||
insert_text: Some("vec![2]".into()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
],
|
||||
item_defaults: Some(lsp::CompletionListItemDefaults {
|
||||
data: Some(default_data.clone()),
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
|
||||
default_edit_range,
|
||||
)),
|
||||
insert_text_format: Some(default_insert_text_format),
|
||||
insert_text_mode: Some(default_insert_text_mode),
|
||||
}),
|
||||
..lsp::CompletionList::default()
|
||||
})))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
let menu = editor.context_menu.read();
|
||||
match menu.as_ref().expect("should have the completions menu") {
|
||||
ContextMenu::Completions(completions_menu) => {
|
||||
assert_eq!(
|
||||
completions_menu
|
||||
.matches
|
||||
.iter()
|
||||
.map(|c| c.string.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Some(2)", "vec![2]"]
|
||||
);
|
||||
}
|
||||
ContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
|
||||
}
|
||||
});
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_first(&ContextMenuFirst, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
2,
|
||||
"After re-selecting the first item, another resolve request should have been sent"
|
||||
);
|
||||
|
||||
expect_first_item.store(false, atomic::Ordering::Release);
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_last(&ContextMenuLast, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"After selecting the other item, another resolve request should have been sent"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user